diff options
Diffstat (limited to 'Utils/src')
91 files changed, 14062 insertions, 0 deletions
diff --git a/Utils/src/Async/AccessSerializer.cs b/Utils/src/Async/AccessSerializer.cs new file mode 100644 index 0000000..ce78f6c --- /dev/null +++ b/Utils/src/Async/AccessSerializer.cs @@ -0,0 +1,297 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: AccessSerializer.cs +* +* AccessSerializer.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; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils.Resources; + +namespace VNLib.Utils.Async +{ + /// <summary> + /// Provides access arbitration to an exclusive resouce + /// </summary> + /// <typeparam name="TKey">The uinique identifier type for the resource</typeparam> + /// <typeparam name="TResource">The resource type</typeparam> + public sealed class AccessSerializer<TKey, TResource> where TResource : IExclusiveResource + { + private readonly SemaphoreSlim semaphore; + private readonly Func<TKey, TResource> Factory; + private readonly Action CompletedCb; + private int WaitingCount; + /// <summary> + /// Creates a new <see cref="AccessSerializer{K, T}"/> with the specified factory and completed callback + /// </summary> + /// <param name="factory">Factory function to genereate new <typeparamref name="TResource"/> objects from <typeparamref name="TKey"/> keys</param> + /// <param name="completedCb">Function to be invoked when the encapsulated objected is no longer in use</param> + /// <exception cref="ArgumentNullException"></exception> + public AccessSerializer(Func<TKey, TResource> factory, Action completedCb) + { + this.Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + this.CompletedCb = completedCb; + //Setup semaphore for locking + this.semaphore = new SemaphoreSlim(1, 1); + this.WaitingCount = 0; + } + + /// <summary> + /// Attempts to obtain an exclusive lock on the object + /// </summary> + /// <param name="key"></param> + /// <param name="wait">Time to wait for lock</param> + /// <param name="exObj"></param> + /// <returns>true if lock was obtained within the timeout, false if the lock was not obtained</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public bool TryWait(TKey key, TimeSpan wait, out ExclusiveResourceHandle<TResource> exObj) + { + //Increase waiting count while we wait + Interlocked.Increment(ref WaitingCount); + try + { + //Try to obtain the lock + if (semaphore.Wait(wait)) + { + TResource get() => Factory(key); + //Create new exclusive lock handle that will generate a new that calls release when freed + exObj = new(get, Release); + return true; + } + //Lock not taken + exObj = null; + return false; + } + finally + { + //Decrease the waiting count since we are no longer waiting + Interlocked.Decrement(ref WaitingCount); + } + } + /// <summary> + /// Waits for exclusive access to the resource. + /// </summary> + /// <param name="key"></param> + /// <returns>An <see cref="ExclusiveResourceHandle{T}"/> encapsulating the resource</returns> + public ExclusiveResourceHandle<TResource> Wait(TKey key) + { + try + { + //Increase waiting count while we wait + Interlocked.Increment(ref WaitingCount); + //Try to obtain the lock + semaphore.Wait(); + //Local function to generate the output value + TResource get() => Factory(key); + //Create new exclusive lock handle that will generate a new that calls release when freed + return new(get, Release); + } + finally + { + //Decrease the waiting count since we are no longer waiting + Interlocked.Decrement(ref WaitingCount); + } + } + /// <summary> + /// Asynchronously waits for exclusive access to the resource. + /// </summary> + /// <returns>An <see cref="ExclusiveResourceHandle{TResource}"/> encapsulating the resource</returns> + public async Task<ExclusiveResourceHandle<TResource>> WaitAsync(TKey key, CancellationToken cancellationToken = default) + { + try + { + //Increase waiting count while we wait + Interlocked.Increment(ref WaitingCount); + //Try to obtain the lock + await semaphore.WaitAsync(cancellationToken); + //Local function to generate the output value + TResource get() => Factory(key); + //Create new exclusive lock handle that will generate a new that calls release when freed + return new(get, Release); + } + finally + { + //Decrease the waiting count since we are no longer waiting + Interlocked.Decrement(ref WaitingCount); + } + } + /// <summary> + /// Releases an exclusive lock that is held on an object + /// </summary> + private void Release() + { + /* + * If objects are waiting for the current instance, then we will release + * the semaphore and exit, as we no longer have control over the context + */ + if (WaitingCount > 0) + { + this.semaphore.Release(); + } + else + { + //Do not release the sempahore, just dispose of the semaphore + this.semaphore.Dispose(); + //call the completed function + CompletedCb?.Invoke(); + } + } + } + + /// <summary> + /// Provides access arbitration to an <see cref="IExclusiveResource"/> + /// </summary> + /// <typeparam name="TKey">The uinique identifier type for the resource</typeparam> + /// <typeparam name="TArg">The type of the optional argument to be passed to the user-implemented factory function</typeparam> + /// <typeparam name="TResource">The resource type</typeparam> + public sealed class AccessSerializer<TKey, TArg, TResource> where TResource : IExclusiveResource + { + private readonly SemaphoreSlim semaphore; + private readonly Func<TKey, TArg, TResource> Factory; + private readonly Action CompletedCb; + private int WaitingCount; + /// <summary> + /// Creates a new <see cref="AccessSerializer{TKey, TArg, TResource}"/> with the specified factory and completed callback + /// </summary> + /// <param name="factory">Factory function to genereate new <typeparamref name="TResource"/> objects from <typeparamref name="TKey"/> keys</param> + /// <param name="completedCb">Function to be invoked when the encapsulated objected is no longer in use</param> + /// <exception cref="ArgumentNullException"></exception> + public AccessSerializer(Func<TKey, TArg, TResource> factory, Action completedCb) + { + this.Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + this.CompletedCb = completedCb; + //Setup semaphore for locking + this.semaphore = new SemaphoreSlim(1, 1); + this.WaitingCount = 0; + } + + /// <summary> + /// Attempts to obtain an exclusive lock on the object + /// </summary> + /// <param name="key"></param> + /// <param name="arg">The key identifying the resource</param> + /// <param name="wait">Time to wait for lock</param> + /// <param name="exObj"></param> + /// <returns>true if lock was obtained within the timeout, false if the lock was not obtained</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public bool TryWait(TKey key, TArg arg, TimeSpan wait, out ExclusiveResourceHandle<TResource> exObj) + { + //Increase waiting count while we wait + Interlocked.Increment(ref WaitingCount); + try + { + //Try to obtain the lock + if (semaphore.Wait(wait)) + { + TResource get() => Factory(key, arg); + //Create new exclusive lock handle that will generate a new that calls release when freed + exObj = new(get, Release); + return true; + } + //Lock not taken + exObj = null; + return false; + } + finally + { + //Decrease the waiting count since we are no longer waiting + Interlocked.Decrement(ref WaitingCount); + } + } + /// <summary> + /// Waits for exclusive access to the resource. + /// </summary> + /// <param name="key">The unique key that identifies the resource</param> + /// <param name="arg">The state argument to pass to the factory function</param> + /// <returns>An <see cref="ExclusiveResourceHandle{TResource}"/> encapsulating the resource</returns> + public ExclusiveResourceHandle<TResource> Wait(TKey key, TArg arg) + { + try + { + //Increase waiting count while we wait + Interlocked.Increment(ref WaitingCount); + //Try to obtain the lock + semaphore.Wait(); + //Local function to generate the output value + TResource get() => Factory(key, arg); + //Create new exclusive lock handle that will generate a new that calls release when freed + return new(get, Release); + } + finally + { + //Decrease the waiting count since we are no longer waiting + Interlocked.Decrement(ref WaitingCount); + } + } + /// <summary> + /// Asynchronously waits for exclusive access to the resource. + /// </summary> + /// <param name="key"></param> + /// <param name="arg">The state argument to pass to the factory function</param> + /// <param name="cancellationToken"></param> + /// <returns>An <see cref="ExclusiveResourceHandle{TResource}"/> encapsulating the resource</returns> + public async Task<ExclusiveResourceHandle<TResource>> WaitAsync(TKey key, TArg arg, CancellationToken cancellationToken = default) + { + try + { + //Increase waiting count while we wait + Interlocked.Increment(ref WaitingCount); + //Try to obtain the lock + await semaphore.WaitAsync(cancellationToken); + //Local function to generate the output value + TResource get() => Factory(key, arg); + //Create new exclusive lock handle that will generate a new that calls release when freed + return new(get, Release); + } + finally + { + //Decrease the waiting count since we are no longer waiting + Interlocked.Decrement(ref WaitingCount); + } + } + + /// <summary> + /// Releases an exclusive lock that is held on an object + /// </summary> + private void Release() + { + /* + * If objects are waiting for the current instance, then we will release + * the semaphore and exit, as we no longer have control over the context + */ + if (WaitingCount > 0) + { + this.semaphore.Release(); + } + else + { + //Do not release the sempahore, just dispose of the semaphore + this.semaphore.Dispose(); + //call the completed function + CompletedCb?.Invoke(); + } + } + } +}
\ No newline at end of file diff --git a/Utils/src/Async/AsyncExclusiveResource.cs b/Utils/src/Async/AsyncExclusiveResource.cs new file mode 100644 index 0000000..18e2a42 --- /dev/null +++ b/Utils/src/Async/AsyncExclusiveResource.cs @@ -0,0 +1,169 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: AsyncExclusiveResource.cs +* +* AsyncExclusiveResource.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; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Utils.Async +{ + /// <summary> + /// Provides a base class for resources that must be obtained exclusivly in a multi-threaded environment + /// but allow state update operations (and their exceptions) to be deferred to the next accessor. + /// </summary> + /// <typeparam name="TState">The state parameter type passed during updates</typeparam> + public abstract class AsyncExclusiveResource<TState> : VnDisposeable, IWaitHandle, IAsyncWaitHandle + { + /// <summary> + /// Main mutli-threading lock used for primary access synchronization + /// </summary> + protected SemaphoreSlim MainLock { get; } = new (1, 1); + + private Task? LastUpdate; + + /// <summary> + /// <inheritdoc/> + /// <br></br> + /// <br></br> + /// If the previous call to <see cref="UpdateAndRelease"/> resulted in an asynchronous update, and exceptions occured, an <see cref="AsyncUpdateException"/> + /// will be thrown enclosing the exception + /// </summary> + /// <param name="millisecondsTimeout">Time in milliseconds to wait for exclusive access to the resource</param> + /// <exception cref="AsyncUpdateException"></exception> + /// <inheritdoc/> + public virtual bool WaitOne(int millisecondsTimeout) + { + //First wait for main lock + if (MainLock.Wait(millisecondsTimeout)) + { + //Main lock has been taken + try + { + //Wait for async update if there is one pending(will throw exceptions if any occurred) + LastUpdate?.Wait(); + return true; + } + catch (AggregateException ae) when (ae.InnerException != null) + { + //Release the main lock and re-throw the inner exception + _ = MainLock.Release(); + //Throw a new async update exception + throw new AsyncUpdateException(ae.InnerException); + } + catch + { + //Release the main lock and re-throw the exception + _ = MainLock.Release(); + throw; + } + } + return false; + } + + ///<inheritdoc/> + ///<exception cref="ObjectDisposedException"></exception> + public virtual async Task WaitOneAsync(CancellationToken token = default) + { + //Wait for main lock + await MainLock.WaitAsync(token).ConfigureAwait(true); + //if the last update completed synchronously, return true + if (LastUpdate == null) + { + return; + } + try + { + //Await the last update task and catch its exceptions + await LastUpdate.ConfigureAwait(false); + } + catch + { + //Release the main lock and re-throw the exception + _ = MainLock.Release(); + throw; + } + } + + /// <summary> + /// Requests a resource update and releases the exclusive lock on this resource. If a deferred update operation has any + /// exceptions during its last operation, they will be thrown here. + /// </summary> + /// <param name="defer">Specifies weather the update should be deferred or awaited on the current call</param> + /// <param name="state">A state parameter to be pased to the update function</param> + /// <exception cref="ObjectDisposedException"></exception> + public async ValueTask UpdateAndRelease(bool defer, TState state) + { + //Otherwise wait and update on the current thread + try + { + //Dispose the update task + LastUpdate?.Dispose(); + //Remove the referrence + LastUpdate = null; + //Run update on the current thread + LastUpdate = await UpdateResource(defer, state).ConfigureAwait(true); + //If the update is not deferred, await the results + if (!defer && LastUpdate != null) + { + await LastUpdate.ConfigureAwait(true); + } + } + finally + { + //Release the main lock + _ = MainLock.Release(); + } + } + + /// <summary> + /// <para> + /// When overrriden in a derived class, is responsible for updating the state of the instance if necessary. + /// </para> + /// <para> + /// If the result of the update retruns a <see cref="Task"/> that represents the deferred update, the next call to <see cref="WaitOne"/> will + /// block until the operation completes and will throw any exceptions that occured + /// </para> + /// </summary> + /// <param name="defer">true if the caller expects a resource update to be deferred, false if the caller expects the result of the update to be awaited</param> + /// <param name="state">State parameter passed when releasing</param> + /// <returns>A <see cref="Task"/> representing the async state update operation, or null if no async state update operation need's to be monitored</returns> + protected abstract ValueTask<Task?> UpdateResource(bool defer, TState state); + + ///<inheritdoc/> + protected override void Free() + { + //Dispose lock + MainLock.Dispose(); + + //Try to cleanup the last update + if (LastUpdate != null && LastUpdate.IsCompletedSuccessfully) + { + LastUpdate.Dispose(); + } + + LastUpdate = null; + } + + } +}
\ No newline at end of file diff --git a/Utils/src/Async/AsyncQueue.cs b/Utils/src/Async/AsyncQueue.cs new file mode 100644 index 0000000..ba45513 --- /dev/null +++ b/Utils/src/Async/AsyncQueue.cs @@ -0,0 +1,144 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: AsyncQueue.cs +* +* AsyncQueue.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; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Channels; +using System.Diagnostics.CodeAnalysis; + +namespace VNLib.Utils.Async +{ + /// <summary> + /// Provides a <see cref="Channel{T}"/> based asynchronous queue + /// </summary> + /// <typeparam name="T">The event object type</typeparam> + public class AsyncQueue<T> + { + private readonly Channel<T> _channel; + + /// <summary> + /// Initalizes a new multi-threaded bound channel queue, that accepts + /// the <paramref name="capacity"/> number of items before it will + /// return asynchronously, or fail to enqueue items + /// </summary> + /// <param name="capacity">The maxium number of items to allow in the queue</param> + public AsyncQueue(int capacity):this(false, false, capacity) + {} + /// <summary> + /// Initalizes a new multi-threaded unbound channel queue + /// </summary> + public AsyncQueue():this(false, false) + {} + + /// <summary> + /// Initalizes a new queue that allows specifying concurrency requirements + /// and a bound/unbound channel capacity + /// </summary> + /// <param name="singleWriter">A value that specifies only a single thread be enqueing items?</param> + /// <param name="singleReader">A value that specifies only a single thread will be dequeing</param> + /// <param name="capacity">The maxium number of items to enque without failing</param> + public AsyncQueue(bool singleWriter, bool singleReader, int capacity = int.MaxValue) + { + if(capacity == int.MaxValue) + { + //Create unbounded + UnboundedChannelOptions opt = new() + { + SingleReader = singleReader, + SingleWriter = singleWriter, + AllowSynchronousContinuations = true, + }; + _channel = Channel.CreateUnbounded<T>(opt); + } + else + { + //Create bounded + BoundedChannelOptions opt = new(capacity) + { + SingleReader = singleReader, + SingleWriter = singleWriter, + AllowSynchronousContinuations = true, + //Default wait for space + FullMode = BoundedChannelFullMode.Wait + }; + _channel = Channel.CreateBounded<T>(opt); + } + } + + /// <summary> + /// Initalizes a new unbound channel based queue + /// </summary> + /// <param name="ubOptions">Channel options</param> + public AsyncQueue(UnboundedChannelOptions ubOptions) + { + _channel = Channel.CreateUnbounded<T>(ubOptions); + } + + /// <summary> + /// Initalizes a new bound channel based queue + /// </summary> + /// <param name="options">Channel options</param> + public AsyncQueue(BoundedChannelOptions options) + { + _channel = Channel.CreateBounded<T>(options); + } + + /// <summary> + /// Attemts to enqeue an item if the queue has the capacity + /// </summary> + /// <param name="item">The item to eqneue</param> + /// <returns>True if the queue can accept another item, false otherwise</returns> + public bool TryEnque(T item) => _channel.Writer.TryWrite(item); + /// <summary> + /// Enqueues an item to the end of the queue and notifies a waiter that an item was enqueued + /// </summary> + /// <param name="item">The item to enqueue</param> + /// <param name="cancellationToken"></param> + /// <exception cref="ObjectDisposedException"></exception> + public ValueTask EnqueueAsync(T item, CancellationToken cancellationToken = default) => _channel.Writer.WriteAsync(item, cancellationToken); + /// <summary> + /// Asynchronously waits for an item to be Enqueued to the end of the queue. + /// </summary> + /// <returns>The item at the begining of the queue</returns> + /// <exception cref="ObjectDisposedException"></exception> + public ValueTask<T> DequeueAsync(CancellationToken cancellationToken = default) => _channel.Reader.ReadAsync(cancellationToken); + /// <summary> + /// Removes the object at the beginning of the queue and stores it to the result parameter. Without waiting for a change + /// event. + /// </summary> + /// <param name="result">The item that was at the begining of the queue</param> + /// <returns>True if the queue could be read synchronously, false if the lock could not be entered, or the queue contains no items</returns> + /// <exception cref="ObjectDisposedException"></exception> + public bool TryDequeue([MaybeNullWhen(false)] out T result) => _channel.Reader.TryRead(out result); + /// <summary> + /// Peeks the object at the beginning of the queue and stores it to the result parameter. Without waiting for a change + /// event. + /// </summary> + /// <param name="result">The item that was at the begining of the queue</param> + /// <returns>True if the queue could be read synchronously, false if the lock could not be entered, or the queue contains no items</returns> + /// <exception cref="ObjectDisposedException"></exception> + public bool TryPeek([MaybeNullWhen(false)] out T result) => _channel.Reader.TryPeek(out result); + } +} diff --git a/Utils/src/Async/AsyncUpdatableResource.cs b/Utils/src/Async/AsyncUpdatableResource.cs new file mode 100644 index 0000000..b4ce519 --- /dev/null +++ b/Utils/src/Async/AsyncUpdatableResource.cs @@ -0,0 +1,111 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: AsyncUpdatableResource.cs +* +* AsyncUpdatableResource.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; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; + +using VNLib.Utils.IO; +using VNLib.Utils.Resources; + +namespace VNLib.Utils.Async +{ + /// <summary> + /// A callback delegate used for updating a <see cref="AsyncUpdatableResource"/> + /// </summary> + /// <param name="source">The <see cref="AsyncUpdatableResource"/> to be updated</param> + /// <param name="data">The serialized data to be stored/updated</param> + /// <exception cref="ResourceUpdateFailedException"></exception> + public delegate Task AsyncUpdateCallback(object source, Stream data); + /// <summary> + /// A callback delegate invoked when a <see cref="AsyncUpdatableResource"/> delete is requested + /// </summary> + /// <param name="source">The <see cref="AsyncUpdatableResource"/> to be deleted</param> + /// <exception cref="ResourceDeleteFailedException"></exception> + public delegate Task AsyncDeleteCallback(object source); + + /// <summary> + /// Implemented by a resource that is backed by an external data store, that when modified or deleted will + /// be reflected to the backing store. + /// </summary> + public abstract class AsyncUpdatableResource : BackedResourceBase, IAsyncExclusiveResource + { + protected abstract AsyncUpdateCallback UpdateCb { get; } + protected abstract AsyncDeleteCallback DeleteCb { get; } + + /// <summary> + /// Releases the resource and flushes pending changes to its backing store. + /// </summary> + /// <returns>A task that represents the async operation</returns> + /// <exception cref="InvalidOperationException"></exception> + /// <exception cref="ResourceDeleteFailedException"></exception> + /// <exception cref="ResourceUpdateFailedException"></exception> + public virtual async ValueTask ReleaseAsync() + { + //If resource has already been realeased, return + if (IsReleased) + { + return; + } + //If deleted flag is set, invoke the delete callback + if (Deleted) + { + await DeleteCb(this).ConfigureAwait(true); + } + //If the state has been modifed, flush changes to the store + else if (Modified) + { + await FlushPendingChangesAsync().ConfigureAwait(true); + } + //Set the released value + IsReleased = true; + } + + /// <summary> + /// <para> + /// Writes the current state of the the resource to the backing store + /// immediatly by invoking the specified callback. + /// </para> + /// <para> + /// Only call this method if your store supports multiple state updates + /// </para> + /// </summary> + protected virtual async Task FlushPendingChangesAsync() + { + //Get the resource + object resource = GetResource(); + //Open a memory stream to store data in + using VnMemoryStream data = new(); + //Serialize and write to stream + VnEncoding.JSONSerializeToBinary(resource, data, resource.GetType(), base.JSO); + //Reset stream to begining + _ = data.Seek(0, SeekOrigin.Begin); + //Invoke update callback + await UpdateCb(this, data).ConfigureAwait(true); + //Clear modified flag + Modified = false; + } + } +} diff --git a/Utils/src/Async/Exceptions/AsyncUpdateException.cs b/Utils/src/Async/Exceptions/AsyncUpdateException.cs new file mode 100644 index 0000000..de5a491 --- /dev/null +++ b/Utils/src/Async/Exceptions/AsyncUpdateException.cs @@ -0,0 +1,52 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: AsyncUpdateException.cs +* +* AsyncUpdateException.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; + +using VNLib.Utils.Resources; + +namespace VNLib.Utils.Async +{ + /// <summary> + /// Represents an exception that was raised during an asyncronous update of a resource. The <see cref="Exception.InnerException"/> stores the + /// details of the actual exception raised + /// </summary> + public sealed class AsyncUpdateException : ResourceUpdateFailedException + { + /// <summary> + /// + /// </summary> + /// <param name="inner"></param> + public AsyncUpdateException(Exception inner) : base("", inner) { } + + public AsyncUpdateException() + {} + + public AsyncUpdateException(string message) : base(message) + {} + + public AsyncUpdateException(string message, Exception innerException) : base(message, innerException) + {} + } +}
\ No newline at end of file diff --git a/Utils/src/Async/IAsyncExclusiveResource.cs b/Utils/src/Async/IAsyncExclusiveResource.cs new file mode 100644 index 0000000..93157ce --- /dev/null +++ b/Utils/src/Async/IAsyncExclusiveResource.cs @@ -0,0 +1,40 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IAsyncExclusiveResource.cs +* +* IAsyncExclusiveResource.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.Threading.Tasks; +using VNLib.Utils.Resources; + +namespace VNLib.Utils.Async +{ + /// <summary> + /// <inheritdoc/> + /// </summary> + public interface IAsyncExclusiveResource : IResource + { + /// <summary> + /// Releases the resource from use. Called when a <see cref="ExclusiveResourceHandle{T}"/> is disposed + /// </summary> + ValueTask ReleaseAsync(); + } +}
\ No newline at end of file diff --git a/Utils/src/Async/IAsyncWaitHandle.cs b/Utils/src/Async/IAsyncWaitHandle.cs new file mode 100644 index 0000000..1cadc06 --- /dev/null +++ b/Utils/src/Async/IAsyncWaitHandle.cs @@ -0,0 +1,41 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IAsyncWaitHandle.cs +* +* IAsyncWaitHandle.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.Threading; +using System.Threading.Tasks; + +namespace VNLib.Utils.Async +{ + /// <summary> + /// Provides a synchronization handle that can be asynchronously aquired + /// </summary> + public interface IAsyncWaitHandle + { + /// <summary> + /// Waits for exclusive access to the resource until the <see cref="CancellationToken"/> expires + /// </summary> + /// <inheritdoc/> + Task WaitOneAsync(CancellationToken token = default); + } +}
\ No newline at end of file diff --git a/Utils/src/Async/IWaitHandle.cs b/Utils/src/Async/IWaitHandle.cs new file mode 100644 index 0000000..85e8a2a --- /dev/null +++ b/Utils/src/Async/IWaitHandle.cs @@ -0,0 +1,59 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IWaitHandle.cs +* +* IWaitHandle.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; +using System.Threading; + +namespace VNLib.Utils.Async +{ + /// <summary> + /// Provides basic thread synchronization functions similar to <see cref="WaitHandle"/> + /// </summary> + public interface IWaitHandle + { + /// <summary> + /// Waits for exclusive access to the resource indefinitly. If the signal is never received this method never returns + /// </summary> + /// <inheritdoc/> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <returns>true if the current thread received the signal</returns> + public virtual bool WaitOne() => WaitOne(Timeout.Infinite); + /// <summary> + /// Waits for exclusive access to the resource until the specified number of milliseconds + /// </summary> + /// <param name="millisecondsTimeout">Time in milliseconds to wait for exclusive access to the resource</param> + /// <returns>true if the current thread received the signal, false if the timout expired, and access was not granted</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + bool WaitOne(int millisecondsTimeout); + /// <summary> + /// Waits for exclusive access to the resource until the specified <see cref="TimeSpan"/> + /// </summary> + /// <returns>true if the current thread received the signal, false if the timout expired, and access was not granted</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public virtual bool WaitOne(TimeSpan timeout) => WaitOne(Convert.ToInt32(timeout.TotalMilliseconds)); + } +}
\ No newline at end of file diff --git a/Utils/src/BitField.cs b/Utils/src/BitField.cs new file mode 100644 index 0000000..bc001df --- /dev/null +++ b/Utils/src/BitField.cs @@ -0,0 +1,115 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: BitField.cs +* +* BitField.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; +using System.Runtime.CompilerServices; + +namespace VNLib.Utils +{ + /// <summary> + /// Represents a field of 64 bits that can be set or cleared using unsigned or signed masks + /// </summary> + public class BitField + { + private ulong Field; + /// <summary> + /// The readonly value of the <see cref="BitField"/> + /// </summary> + public ulong Value => Field; + /// <summary> + /// Creates a new <see cref="BitField"/> initialized to the specified value + /// </summary> + /// <param name="initial">Initial value</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BitField(ulong initial) => Field = initial; + /// <summary> + /// Creates a new <see cref="BitField"/> initialized to the specified value + /// </summary> + /// <param name="initial">Initial value</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BitField(long initial) => Field = unchecked((ulong)initial); + /// <summary> + /// Determines if the specified flag is set + /// </summary> + /// <param name="mask">The mask to compare against the field value</param> + /// <returns>True if the flag(s) is currently set, false if flag is not set</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsSet(ulong mask) => (Field & mask) != 0; + /// <summary> + /// Determines if the specified flag is set + /// </summary> + /// <param name="mask">The mask to compare against the field value</param> + /// <returns>True if the flag(s) is currently set, false if flag is not set</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsSet(long mask) => (Field & unchecked((ulong)mask)) != 0; + /// <summary> + /// Determines if the specified flag is set + /// </summary> + /// <param name="mask">The mask to compare against the field value</param> + /// <returns>True if the flag(s) is currently set, false if flag is not set</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(ulong mask) => Field |= mask; + /// <summary> + /// Determines if the specified flag is set + /// </summary> + /// <param name="mask">The mask to compare against the field value</param> + /// <returns>True if the flag(s) is currently set, false if flag is not set</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(long mask) => Field |= unchecked((ulong)mask); + /// <summary> + /// Sets or clears a flag(s) indentified by a mask based on the value + /// </summary> + /// <param name="mask">Mask used to identify flags</param> + /// <param name="value">True to set a flag, false to clear a flag</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(ulong mask, bool value) + { + if (value) + { + Set(mask); + } + else + { + Clear(mask); + } + } + /// <summary> + /// Clears the flag identified by the specified mask + /// </summary> + /// <param name="mask">The mask used to clear the given flag</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear(ulong mask) => Field &= ~mask; + /// <summary> + /// Clears the flag identified by the specified mask + /// </summary> + /// <param name="mask">The mask used to clear the given flag</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear(long mask) => Field &= ~unchecked((ulong)mask); + /// <summary> + /// Clears all flags by setting the <see cref="Field"/> property value to 0 + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearAll() => Field = 0; + } +}
\ No newline at end of file diff --git a/Utils/src/ERRNO.cs b/Utils/src/ERRNO.cs new file mode 100644 index 0000000..c3c61de --- /dev/null +++ b/Utils/src/ERRNO.cs @@ -0,0 +1,152 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ERRNO.cs +* +* ERRNO.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; +using System.Runtime.InteropServices; + +namespace VNLib.Utils +{ + /// <summary> + /// Implements a C style integer error code type. Size is platform dependent + /// </summary> + [StructLayout(LayoutKind.Sequential)] + public readonly struct ERRNO : IEquatable<ERRNO>, ISpanFormattable, IFormattable + { + /// <summary> + /// Represents a successfull error code (true) + /// </summary> + public static readonly ERRNO SUCCESS = true; + + /// <summary> + /// Represents a failure error code (false) + /// </summary> + public static readonly ERRNO E_FAIL = false; + + private readonly nint ErrorCode; + /// <summary> + /// Creates a new <see cref="ERRNO"/> from the specified error value + /// </summary> + /// <param name="errno">The value of the error to represent</param> + public ERRNO(nint errno) => ErrorCode = errno; + /// <summary> + /// Creates a new <see cref="ERRNO"/> from an <see cref="int"/> error code. null = 0 = false + /// </summary> + /// <param name="errorVal">Error code</param> + public static implicit operator ERRNO(int errorVal) => new (errorVal); + /// <summary> + /// Creates a new <see cref="ERRNO"/> from an <see cref="int"/> error code. null = 0 = false + /// </summary> + /// <param name="errorVal">Error code</param> + public static explicit operator ERRNO(int? errorVal) => new(errorVal ?? 0); + /// <summary> + /// Creates a new <see cref="ERRNO"/> from a booleam, 1 if true, 0 if false + /// </summary> + /// <param name="errorVal"></param> + public static implicit operator ERRNO(bool errorVal) => new(errorVal ? 1 : 0); + /// <summary> + /// Creates a new <see cref="ERRNO"/> from a pointer value + /// </summary> + /// <param name="errno">The pointer value representing an error code</param> + public static implicit operator ERRNO(nint errno) => new(errno); + /// <summary> + /// Error value as integer. Value of supplied error code or if cast from boolean 1 if true, 0 if false + /// </summary> + /// <param name="errorVal"><see cref="ERRNO"/> to get error code from</param> + public static implicit operator int(ERRNO errorVal) => (int)errorVal.ErrorCode; + /// <summary> + /// C style boolean conversion. false if 0, true otherwise + /// </summary> + /// <param name="errorVal"></param> + public static implicit operator bool(ERRNO errorVal) => errorVal != 0; + /// <summary> + /// Creates a new <see cref="IntPtr"/> from the value if the stored (nint) error code + /// </summary> + /// <param name="errno">The <see cref="ERRNO"/> contating the pointer value</param> + public static implicit operator IntPtr(ERRNO errno) => new(errno.ErrorCode); + /// <summary> + /// Creates a new <c>nint</c> from the value if the stored error code + /// </summary> + /// <param name="errno">The <see cref="ERRNO"/> contating the pointer value</param> + public static implicit operator nint(ERRNO errno) => errno.ErrorCode; + + public static ERRNO operator +(ERRNO err, int add) => new(err.ErrorCode + add); + public static ERRNO operator +(ERRNO err, nint add) => new(err.ErrorCode + add); + public static ERRNO operator ++(ERRNO err) => new(err.ErrorCode + 1); + public static ERRNO operator --(ERRNO err) => new(err.ErrorCode - 1); + public static ERRNO operator -(ERRNO err, int subtract) => new(err.ErrorCode - subtract); + public static ERRNO operator -(ERRNO err, nint subtract) => new(err.ErrorCode - subtract); + + public static bool operator >(ERRNO err, ERRNO other) => err.ErrorCode > other.ErrorCode; + public static bool operator <(ERRNO err, ERRNO other) => err.ErrorCode < other.ErrorCode; + public static bool operator >=(ERRNO err, ERRNO other) => err.ErrorCode >= other.ErrorCode; + public static bool operator <=(ERRNO err, ERRNO other) => err.ErrorCode <= other.ErrorCode; + + public static bool operator >(ERRNO err, int other) => err.ErrorCode > other; + public static bool operator <(ERRNO err, int other) => err.ErrorCode < other; + public static bool operator >=(ERRNO err, int other) => err.ErrorCode >= other; + public static bool operator <=(ERRNO err, int other) => err.ErrorCode <= other; + + public static bool operator >(ERRNO err, nint other) => err.ErrorCode > other; + public static bool operator <(ERRNO err, nint other) => err.ErrorCode < other; + public static bool operator >=(ERRNO err, nint other) => err.ErrorCode >= other; + public static bool operator <=(ERRNO err, nint other) => err.ErrorCode <= other; + + public static bool operator ==(ERRNO err, ERRNO other) => err.ErrorCode == other.ErrorCode; + public static bool operator !=(ERRNO err, ERRNO other) => err.ErrorCode != other.ErrorCode; + public static bool operator ==(ERRNO err, int other) => err.ErrorCode == other; + public static bool operator !=(ERRNO err, int other) => err.ErrorCode != other; + public static bool operator ==(ERRNO err, nint other) => err.ErrorCode == other; + public static bool operator !=(ERRNO err, nint other) => err.ErrorCode != other; + + public readonly bool Equals(ERRNO other) => ErrorCode == other.ErrorCode; + public readonly override bool Equals(object obj) => obj is ERRNO other && Equals(other); + public readonly override int GetHashCode() => ErrorCode.GetHashCode(); + + /// <summary> + /// The integer error value of the current instance in radix 10 + /// </summary> + /// <returns></returns> + public readonly override string ToString() + { + //Return the string of the error code number + return ErrorCode.ToString(); + } + public readonly string ToString(string format) + { + //Return the string of the error code number + return ErrorCode.ToString(format); + } + + ///<inheritdoc/> + public readonly bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider provider) + { + return ErrorCode.TryFormat(destination, out charsWritten, format, provider); + } + ///<inheritdoc/> + public readonly string ToString(string format, IFormatProvider formatProvider) + { + return ErrorCode.ToString(format, formatProvider); + } + } +} diff --git a/Utils/src/Extensions/CacheExtensions.cs b/Utils/src/Extensions/CacheExtensions.cs new file mode 100644 index 0000000..5485c2d --- /dev/null +++ b/Utils/src/Extensions/CacheExtensions.cs @@ -0,0 +1,348 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: CacheExtensions.cs +* +* CacheExtensions.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; +using System.Linq; +using System.Collections.Generic; + +using VNLib.Utils.Memory.Caching; + +namespace VNLib.Utils.Extensions +{ + /// <summary> + /// Cache collection extensions + /// </summary> + public static class CacheExtensions + { + /// <summary> + /// <para> + /// Stores a new record. If an old record exists, the records are compared, + /// if they are not equal, the old record is evicted and the new record is stored + /// </para> + /// </summary> + /// <typeparam name="TKey"></typeparam> + /// <typeparam name="T">A cachable object</typeparam> + /// <param name="store"></param> + /// <param name="key">The unique key identifying the record</param> + /// <param name="record">The record to store</param> + /// <remarks> + /// Locks on the store parameter to provide mutual exclusion for non thread-safe + /// data structures. + /// </remarks> + public static void StoreRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key, T record) where T : ICacheable + { + T ?oldRecord = default; + lock (store) + { + //See if an old record exists + if (!store.Remove(key, out oldRecord) || oldRecord == null) + { + //Old record doesnt exist, store and return + store[key] = record; + return; + } + //See if the old and new records and the same record + if (oldRecord.Equals(record)) + { + //records are equal, so we can exit + return; + } + //Old record is not equal, so we can store the new record and evict the old on + store[key] = record; + } + //Call evict on the old record + oldRecord.Evicted(); + } + /// <summary> + /// <para> + /// Stores a new record and updates the expiration date. If an old record exists, the records + /// are compared, if they are not equal, the old record is evicted and the new record is stored + /// </para> + /// </summary> + /// <typeparam name="TKey"></typeparam> + /// <typeparam name="T">A cachable object</typeparam> + /// <param name="store"></param> + /// <param name="key">The unique key identifying the record</param> + /// <param name="record">The record to store</param> + /// <param name="validFor">The new expiration time of the record</param> + /// <remarks> + /// Locks on the store parameter to provide mutual exclusion for non thread-safe + /// data structures. + /// </remarks> + public static void StoreRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key, T record, TimeSpan validFor) where T : ICacheable + { + //Update the expiration time + record.Expires = DateTime.UtcNow.Add(validFor); + //Store + StoreRecord(store, key, record); + } + /// <summary> + /// <para> + /// Returns a stored record if it exists and is not expired. If the record exists + /// but has expired, it is evicted. + /// </para> + /// <para> + /// If a record is evicted, the return value evaluates to -1 and the value parameter + /// is set to the old record if the caller wished to inspect the record after the + /// eviction method completes + /// </para> + /// </summary> + /// <typeparam name="TKey"></typeparam> + /// <typeparam name="T">A cachable object</typeparam> + /// <param name="store"></param> + /// <param name="key"></param> + /// <param name="value">The record</param> + /// <returns> + /// Gets a value indicating the reults of the operation. 0 if the record is not found, -1 if expired, 1 if + /// record is valid + /// </returns> + /// <remarks> + /// Locks on the store parameter to provide mutual exclusion for non thread-safe + /// data structures. + /// </remarks> + public static ERRNO TryGetOrEvictRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key, out T? value) where T : ICacheable + { + value = default; + //Cache current date time before entering the lock + DateTime now = DateTime.UtcNow; + //Get value + lock (store) + { + //try to get the value + if (!store.TryGetValue(key, out value)) + { + //not found + return 0; + } + //Not expired + if (value.Expires > now) + { + return true; + } + //Remove from store + _ = store.Remove(key); + } + //Call the evict func + value.Evicted(); + return -1; + } + /// <summary> + /// Updates the expiration date on a record to the specified time if it exists, regardless + /// of its validity + /// </summary> + /// <typeparam name="TKey">Diction key type</typeparam> + /// <typeparam name="T">A cachable object</typeparam> + /// <param name="store"></param> + /// <param name="key">The unique key identifying the record to update</param> + /// <param name="extendedTime">The expiration time (time added to <see cref="DateTime.UtcNow"/>)</param> + /// <remarks> + /// Locks on the store parameter to provide mutual exclusion for non thread-safe + /// data structures. + /// </remarks> + public static void UpdateRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key, TimeSpan extendedTime) where T : ICacheable + { + //Cacl the expiration time + DateTime expiration = DateTime.UtcNow.Add(extendedTime); + lock (store) + { + //Update the expiration time if the record exists + if (store.TryGetValue(key, out T? record) && record != null) + { + record.Expires = expiration; + } + } + } + /// <summary> + /// Evicts a stored record from the store. If the record is found, the eviction + /// method is executed + /// </summary> + /// <typeparam name="TKey"></typeparam> + /// <typeparam name="T"></typeparam> + /// <param name="store"></param> + /// <param name="key">The unique key identifying the record</param> + /// <returns>True if the record was found and evicted</returns> + public static bool EvictRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key) where T : ICacheable + { + T? record = default; + lock (store) + { + //Try to remove the record + if (!store.Remove(key, out record) || record == null) + { + //No record found or null + return false; + } + } + //Call eviction mode + record.Evicted(); + return true; + } + /// <summary> + /// Evicts all expired records from the store + /// </summary> + /// <typeparam name="TKey"></typeparam> + /// <typeparam name="T"></typeparam> + public static void CollectRecords<TKey, T>(this IDictionary<TKey, T> store) where T : ICacheable + { + CollectRecords(store, DateTime.UtcNow); + } + + /// <summary> + /// Evicts all expired records from the store + /// </summary> + /// <typeparam name="TKey"></typeparam> + /// <typeparam name="T"></typeparam> + /// <param name="store"></param> + /// <param name="validAfter">A time that specifies the time which expired records should be evicted</param> + public static void CollectRecords<TKey, T>(this IDictionary<TKey, T> store, DateTime validAfter) where T : ICacheable + { + //Build a query to get the keys that belong to the expired records + IEnumerable<KeyValuePair<TKey, T>> expired = store.Where(s => s.Value.Expires < validAfter); + //temp list for expired records + IEnumerable<T> evicted; + //Take lock on store + lock (store) + { + KeyValuePair<TKey, T>[] kvp = expired.ToArray(); + //enumerate to array so values can be removed while the lock is being held + foreach (KeyValuePair<TKey, T> pair in kvp) + { + //remove the record and call the eviction method + _ = store.Remove(pair); + } + //select values while lock held + evicted = kvp.Select(static v => v.Value); + } + //Iterrate over evicted records and call evicted method + foreach (T ev in evicted) + { + ev.Evicted(); + } + } + + /// <summary> + /// Allows for mutually exclusive use of a <see cref="ICacheable"/> record with a + /// state parameter + /// </summary> + /// <typeparam name="TKey"></typeparam> + /// <typeparam name="T"></typeparam> + /// <typeparam name="State"></typeparam> + /// <param name="store"></param> + /// <param name="key">The unique key identifying the record</param> + /// <param name="state">A user-token type state parameter to pass to the use callback method</param> + /// <param name="useCtx">A callback method that will be passed the record to use within an exclusive context</param> + public static void UseRecord<TKey, T, State>(this IDictionary<TKey, T> store, TKey key, State state, Action<T, State> useCtx) where T: ICacheable + { + lock (store) + { + //If the record exists + if(store.TryGetValue(key, out T record)) + { + //Use it within the lock statement + useCtx(record, state); + } + } + } + /// <summary> + /// Allows for mutually exclusive use of a <see cref="ICacheable"/> + /// </summary> + /// <typeparam name="TKey"></typeparam> + /// <typeparam name="T"></typeparam> + /// <param name="store"></param> + /// <param name="key">The unique key identifying the record</param> + /// <param name="useCtx">A callback method that will be passed the record to use within an exclusive context</param> + public static void UseRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key, Action<T> useCtx) where T : ICacheable + { + lock (store) + { + //If the record exists + if (store.TryGetValue(key, out T record)) + { + //Use it within the lock statement + useCtx(record); + } + } + } + /// <summary> + /// Allows for mutually exclusive use of a <see cref="ICacheable"/> record with a + /// state parameter, only if the found record is valid + /// </summary> + /// <typeparam name="TKey"></typeparam> + /// <typeparam name="T"></typeparam> + /// <typeparam name="State"></typeparam> + /// <param name="store"></param> + /// <param name="key">The unique key identifying the record</param> + /// <param name="state">A user-token type state parameter to pass to the use callback method</param> + /// <param name="useCtx">A callback method that will be passed the record to use within an exclusive context</param> + /// <remarks>If the record is found, but is expired, the record is evicted from the store. The callback is never invoked</remarks> + public static void UseIfValid<TKey, T, State>(this IDictionary<TKey, T> store, TKey key, State state, Action<T, State> useCtx) where T : ICacheable + { + DateTime now = DateTime.UtcNow; + T? record; + lock (store) + { + //If the record exists, check if its valid + if (store.TryGetValue(key, out record) && record.Expires < now) + { + //Use it within the lock statement + useCtx(record, state); + return; + } + //Record is no longer valid + _ = store.Remove(key); + } + //Call evicted method + record?.Evicted(); + } + /// <summary> + /// Allows for mutually exclusive use of a <see cref="ICacheable"/> record with a + /// state parameter, only if the found record is valid + /// </summary> + /// <typeparam name="TKey"></typeparam> + /// <typeparam name="T"></typeparam> + /// <param name="store"></param> + /// <param name="key">The unique key identifying the record</param> + /// <param name="useCtx">A callback method that will be passed the record to use within an exclusive context</param> + /// <remarks>If the record is found, but is expired, the record is evicted from the store. The callback is never invoked</remarks> + public static void UseIfValid<TKey, T>(this IDictionary<TKey, T> store, TKey key, Action<T> useCtx) where T : ICacheable + { + DateTime now = DateTime.UtcNow; + T? record; + lock (store) + { + //If the record exists, check if its valid + if (store.TryGetValue(key, out record) && record.Expires < now) + { + //Use it within the lock statement + useCtx(record); + return; + } + //Record is no longer valid + _ = store.Remove(key); + } + //Call evicted method + record?.Evicted(); + } + } +} diff --git a/Utils/src/Extensions/CollectionExtensions.cs b/Utils/src/Extensions/CollectionExtensions.cs new file mode 100644 index 0000000..e4ec459 --- /dev/null +++ b/Utils/src/Extensions/CollectionExtensions.cs @@ -0,0 +1,98 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: CollectionExtensions.cs +* +* CollectionExtensions.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; +using System.Collections.Generic; + +using VNLib.Utils.Memory; + +namespace VNLib.Utils.Extensions +{ + /// <summary> + /// Provides collection extension methods + /// </summary> + public static class CollectionExtensions + { + /// <summary> + /// Gets a previously-stored base32 encoded value-type from the lookup and returns its initialized structure from + /// the value stored + /// </summary> + /// <typeparam name="TKey">The key type used to index the lookup</typeparam> + /// <typeparam name="TValue">An unmanaged structure type</typeparam> + /// <param name="lookup"></param> + /// <param name="key">The key used to identify the value</param> + /// <returns>The initialized structure, or default if the lookup returns null/empty string</returns> + public static TValue GetValueType<TKey, TValue>(this IIndexable<TKey, string> lookup, TKey key) where TValue : unmanaged where TKey : notnull + { + //Get value + string value = lookup[key]; + //If the string is set, recover the value and return it + return string.IsNullOrWhiteSpace(value) ? default : VnEncoding.FromBase32String<TValue>(value); + } + + /// <summary> + /// Serializes a value-type in base32 encoding and stores it at the specified key + /// </summary> + /// <typeparam name="TKey">The key type used to index the lookup</typeparam> + /// <typeparam name="TValue">An unmanaged structure type</typeparam> + /// <param name="lookup"></param> + /// <param name="key">The key used to identify the value</param> + /// <param name="value">The value to serialze</param> + public static void SetValueType<TKey, TValue>(this IIndexable<TKey, string> lookup, TKey key, TValue value) where TValue : unmanaged where TKey : notnull + { + //encode string from value type and store in lookup + lookup[key] = VnEncoding.ToBase32String(value); + } + /// <summary> + /// Executes a handler delegate on every element of the list within a try-catch block + /// and rethrows exceptions as an <see cref="AggregateException"/> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="list"></param> + /// <param name="handler">An <see cref="Action"/> handler delegate to complete some operation on the elements within the list</param> + /// <exception cref="AggregateException"></exception> + public static void TryForeach<T>(this IEnumerable<T> list, Action<T> handler) + { + List<Exception>? exceptionList = null; + foreach(T item in list) + { + try + { + handler(item); + } + catch(Exception ex) + { + //Init new list and add the exception + exceptionList ??= new(); + exceptionList.Add(ex); + } + } + //Raise aggregate exception for all caught exceptions + if(exceptionList?.Count > 0) + { + throw new AggregateException(exceptionList); + } + } + } +} diff --git a/Utils/src/Extensions/IoExtensions.cs b/Utils/src/Extensions/IoExtensions.cs new file mode 100644 index 0000000..f312203 --- /dev/null +++ b/Utils/src/Extensions/IoExtensions.cs @@ -0,0 +1,345 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IoExtensions.cs +* +* IoExtensions.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; +using System.IO; +using System.Buffers; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.Versioning; +using System.Runtime.CompilerServices; + +using VNLib.Utils.IO; +using VNLib.Utils.Memory; + +using static VNLib.Utils.Memory.Memory; + +namespace VNLib.Utils.Extensions +{ + /// <summary> + /// Provieds extension methods for common IO operations + /// </summary> + public static class IoExtensions + { + /// <summary> + /// Unlocks the entire file + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public static void Unlock(this FileStream fs) + { + _ = fs ?? throw new ArgumentNullException(nameof(fs)); + //Unlock the entire file + fs.Unlock(0, fs.Length); + } + + /// <summary> + /// Locks the entire file + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public static void Lock(this FileStream fs) + { + _ = fs ?? throw new ArgumentNullException(nameof(fs)); + //Lock the entire length of the file + fs.Lock(0, fs.Length); + } + + /// <summary> + /// Provides an async wrapper for copying data from the current stream to another using an unmanged + /// buffer. + /// </summary> + /// <param name="source"></param> + /// <param name="dest">The destination data stream to write data to</param> + /// <param name="bufferSize">The size of the buffer to use while copying data. (Value will be clamped to the size of the stream if seeking is available)</param> + /// <param name="heap">The <see cref="IUnmangedHeap"/> to allocate the buffer from</param> + /// <param name="token">A token that may cancel asynchronous operations</param> + /// <returns>A <see cref="ValueTask"/> that completes when the copy operation has completed</returns> + /// <exception cref="IOException"></exception> + /// <exception cref="ArgumentException"></exception> + public static async ValueTask CopyToAsync(this Stream source, Stream dest, int bufferSize, IUnmangedHeap heap, CancellationToken token = default) + { + if (source.CanSeek) + { + bufferSize = (int)Math.Min(source.Length, bufferSize); + } + //Alloc a buffer + using IMemoryOwner<byte> buffer = heap.DirectAlloc<byte>(bufferSize); + //Wait for copy to complete + await CopyToAsync(source, dest, buffer.Memory, token); + } + /// <summary> + /// Provides an async wrapper for copying data from the current stream to another with a + /// buffer from the <paramref name="heap"/> + /// </summary> + /// <param name="source"></param> + /// <param name="dest">The destination data stream to write data to</param> + /// <param name="bufferSize">The size of the buffer to use while copying data. (Value will be clamped to the size of the stream if seeking is available)</param> + /// <param name="count">The number of bytes to copy from the current stream to destination stream</param> + /// <param name="heap">The heap to alloc buffer from</param> + /// <param name="token">A token that may cancel asynchronous operations</param> + /// <returns>A <see cref="ValueTask"/> that completes when the copy operation has completed</returns> + /// <exception cref="IOException"></exception> + /// <exception cref="ArgumentException"></exception> + public static async ValueTask CopyToAsync(this Stream source, Stream dest, long count, int bufferSize, IUnmangedHeap heap, CancellationToken token = default) + { + if (source.CanSeek) + { + bufferSize = (int)Math.Min(source.Length, bufferSize); + } + //Alloc a buffer + using IMemoryOwner<byte> buffer = heap.DirectAlloc<byte>(bufferSize); + //Wait for copy to complete + await CopyToAsync(source, dest, buffer.Memory, count, token); + } + + /// <summary> + /// Copies data from one stream to another, using self managed buffers. May allocate up to 2MB. + /// </summary> + /// <param name="source">Source stream to read from</param> + /// <param name="dest">Destination stream to write data to</param> + /// <param name="heap">The heap to allocate buffers from</param> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> + public static void CopyTo(this Stream source, Stream dest, IUnmangedHeap? heap = null) + { + if (!source.CanRead) + { + throw new ArgumentException("Source stream is unreadable", nameof(source)); + } + if (!dest.CanWrite) + { + throw new ArgumentException("Destination stream is unwritable", nameof(dest)); + } + heap ??= Shared; + //Get a buffer size, maximum of 2mb buffer size if the stream supports seeking, otherwise, min buf size + int bufSize = source.CanSeek ? (int)Math.Min(source.Length, MAX_BUF_SIZE) : MIN_BUF_SIZE; + //Length must be 0, so return + if (bufSize == 0) + { + return; + } + //Alloc a buffer + using UnsafeMemoryHandle<byte> buffer = heap.UnsafeAlloc<byte>(bufSize); + int read; + do + { + //read + read = source.Read(buffer.Span); + //Guard + if (read == 0) + { + break; + } + //write only the data that was read (slice) + dest.Write(buffer.Span[..read]); + } while (true); + } + /// <summary> + /// Copies data from one stream to another, using self managed buffers. May allocate up to 2MB. + /// </summary> + /// <param name="source">Source stream to read from</param> + /// <param name="dest">Destination stream to write data to</param> + /// <param name="count">Number of bytes to read/write</param> + /// <param name="heap">The heap to allocate buffers from</param> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> + public static void CopyTo(this Stream source, Stream dest, long count, IUnmangedHeap? heap = null) + { + if (!source.CanRead) + { + throw new ArgumentException("Source stream is unreadable", nameof(source)); + } + if (!dest.CanWrite) + { + throw new ArgumentException("Destination stream is unwritable", nameof(dest)); + } + //Set default heap + heap ??= Shared; + //Get a buffer size, maximum of 2mb buffer size if the stream supports seeking, otherwise, min buf size + int bufSize = source.CanSeek ? (int)Math.Min(source.Length, MAX_BUF_SIZE) : MIN_BUF_SIZE; + //Length must be 0, so return + if (bufSize == 0) + { + return; + } + //Alloc a buffer + using UnsafeMemoryHandle<byte> buffer = heap.UnsafeAlloc<byte>(bufSize); + //wrapper around offset pointer + long total = 0; + int read; + do + { + Span<byte> wrapper = buffer.Span[..(int)Math.Min(bufSize, (count - total))]; + //read + read = source.Read(wrapper); + //Guard + if (read == 0) + { + break; + } + //write only the data that was read (slice) + dest.Write(wrapper[..read]); + //Update total + total += read; + } while (true); + } + + /// <summary> + /// Copies data from the current stream to the destination stream using the supplied memory buffer + /// </summary> + /// <param name="source"></param> + /// <param name="dest">The destination data stream to write data to</param> + /// <param name="buffer">The buffer to use when copying data</param> + /// <param name="token">A token that may cancel asynchronous operations</param> + /// <returns>A <see cref="ValueTask"/> that completes when the copy operation has completed</returns> + /// <exception cref="ArgumentException"></exception> + public static async ValueTask CopyToAsync(this Stream source, Stream dest, Memory<byte> buffer, CancellationToken token = default) + { + //Make sure source can be read from, and dest can be written to + if (!source.CanRead) + { + throw new ArgumentException("Source stream is unreadable", nameof(source)); + } + if (!dest.CanWrite) + { + throw new ArgumentException("Destination stream is unwritable", nameof(dest)); + } + //Read in loop + int read; + while (true) + { + //read + read = await source.ReadAsync(buffer, token); + //Guard + if (read == 0) + { + break; + } + //write only the data that was read (slice) + await dest.WriteAsync(buffer[..read], token); + } + } + + /// <summary> + /// Copies data from the current stream to the destination stream using the supplied memory buffer + /// </summary> + /// <param name="source"></param> + /// <param name="dest">The destination data stream to write data to</param> + /// <param name="buffer">The buffer to use when copying data</param> + /// <param name="count">The number of bytes to copy from the current stream to destination stream</param> + /// <param name="token">A token that may cancel asynchronous operations</param> + /// <returns>A <see cref="ValueTask"/> that completes when the copy operation has completed</returns> + /// <exception cref="ArgumentException"></exception> + public static async ValueTask CopyToAsync(this Stream source, Stream dest, Memory<byte> buffer, long count, CancellationToken token = default) + { + //Make sure source can be read from, and dest can be written to + if (!source.CanRead) + { + throw new ArgumentException("Source stream is unreadable", nameof(source)); + } + if (!dest.CanWrite) + { + throw new ArgumentException("Destination stream is unwritable", nameof(dest)); + } + /* + * Track total count so we copy the exect number of + * bytes from the source + */ + long total = 0; + int bufferSize = buffer.Length; + int read; + while (true) + { + //get offset wrapper of the total buffer or remaining count + Memory<byte> offset = buffer[..(int)Math.Min(bufferSize, count - total)]; + //read + read = await source.ReadAsync(offset, token); + //Guard + if (read == 0) + { + break; + } + //write only the data that was read (slice) + await dest.WriteAsync(offset[..read], token); + //Update total + total += read; + } + } + + /// <summary> + /// Opens a file within the current directory + /// </summary> + /// <param name="dir"></param> + /// <param name="fileName">The name of the file to open</param> + /// <param name="mode">The <see cref="FileMode"/> to open the file with</param> + /// <param name="access">The <see cref="FileAccess"/> to open the file with</param> + /// <param name="share"></param> + /// <param name="bufferSize">The size of the buffer to read/write with</param> + /// <param name="options"></param> + /// <returns>The <see cref="FileStream"/> of the opened file</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FileStream OpenFile(this DirectoryInfo dir, + string fileName, + FileMode mode, + FileAccess access, + FileShare share = FileShare.None, + int bufferSize = 4096, + FileOptions options = FileOptions.None) + { + _ = dir ?? throw new ArgumentNullException(nameof(dir)); + string fullPath = Path.Combine(dir.FullName, fileName); + return new FileStream(fullPath, mode, access, share, bufferSize, options); + } + /// <summary> + /// Deletes the speicifed file from the current directory + /// </summary> + /// <param name="dir"></param> + /// <param name="fileName">The name of the file to delete</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void DeleteFile(this DirectoryInfo dir, string fileName) + { + _ = dir ?? throw new ArgumentNullException(nameof(dir)); + string fullPath = Path.Combine(dir.FullName, fileName); + File.Delete(fullPath); + } + /// <summary> + /// Determines if a file exists within the current directory + /// </summary> + /// <param name="dir"></param> + /// <param name="fileName">The name of the file to search for</param> + /// <returns>True if the file is found and the user has permission to access the file, false otherwise</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool FileExists(this DirectoryInfo dir, string fileName) + { + _ = dir ?? throw new ArgumentNullException(nameof(dir)); + string fullPath = Path.Combine(dir.FullName, fileName); + return FileOperations.FileExists(fullPath); + } + } +}
\ No newline at end of file diff --git a/Utils/src/Extensions/JsonExtensions.cs b/Utils/src/Extensions/JsonExtensions.cs new file mode 100644 index 0000000..a27dcc0 --- /dev/null +++ b/Utils/src/Extensions/JsonExtensions.cs @@ -0,0 +1,215 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: JsonExtensions.cs +* +* JsonExtensions.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; +using System.Text.Json; +using System.Collections.Generic; + +using VNLib.Utils.IO; + +namespace VNLib.Utils.Extensions +{ + /// <summary> + /// Specifies how to parse a timespan value from a <see cref="JsonDocument"/> element + /// </summary> + public enum TimeParseType + { + Milliseconds, + Seconds, + Minutes, + Hours, + Days, + Ticks + } + + public static class JsonExtensions + { + /// <summary> + /// Converts a JSON encoded string to an object of the specified type + /// </summary> + /// <typeparam name="T">Output type of the object</typeparam> + /// <param name="value"></param> + /// <param name="options"><see cref="JsonSerializerOptions"/> to use during de-serialization</param> + /// <returns>The new object or default if the string is null or empty</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static T? AsJsonObject<T>(this string value, JsonSerializerOptions? options = null) + { + return !string.IsNullOrWhiteSpace(value) ? JsonSerializer.Deserialize<T>(value, options) : default; + } + /// <summary> + /// Converts a JSON encoded binary data to an object of the specified type + /// </summary> + /// <typeparam name="T">Output type of the object</typeparam> + /// <param name="utf8bin"></param> + /// <param name="options"><see cref="JsonSerializerOptions"/> to use during de-serialization</param> + /// <returns>The new object or default if the string is null or empty</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static T? AsJsonObject<T>(this in ReadOnlySpan<byte> utf8bin, JsonSerializerOptions? options = null) + { + return utf8bin.IsEmpty ? default : JsonSerializer.Deserialize<T>(utf8bin, options); + } + /// <summary> + /// Converts a JSON encoded binary data to an object of the specified type + /// </summary> + /// <typeparam name="T">Output type of the object</typeparam> + /// <param name="utf8bin"></param> + /// <param name="options"><see cref="JsonSerializerOptions"/> to use during de-serialization</param> + /// <returns>The new object or default if the string is null or empty</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static T? AsJsonObject<T>(this in ReadOnlyMemory<byte> utf8bin, JsonSerializerOptions? options = null) + { + return utf8bin.IsEmpty ? default : JsonSerializer.Deserialize<T>(utf8bin.Span, options); + } + /// <summary> + /// Converts a JSON encoded binary data to an object of the specified type + /// </summary> + /// <typeparam name="T">Output type of the object</typeparam> + /// <param name="utf8bin"></param> + /// <param name="options"><see cref="JsonSerializerOptions"/> to use during de-serialization</param> + /// <returns>The new object or default if the string is null or empty</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static T? AsJsonObject<T>(this byte[] utf8bin, JsonSerializerOptions? options = null) + { + return utf8bin == null ? default : JsonSerializer.Deserialize<T>(utf8bin.AsSpan(), options); + } + /// <summary> + /// Parses a json encoded string to a json documen + /// </summary> + /// <param name="jsonString"></param> + /// <param name="options"></param> + /// <returns>If the json string is null, returns null, otherwise the json document around the data</returns> + /// <exception cref="JsonException"></exception> + public static JsonDocument? AsJsonDocument(this string jsonString, JsonDocumentOptions options = default) + { + return jsonString == null ? null : JsonDocument.Parse(jsonString, options); + } + /// <summary> + /// Shortcut extension to <see cref="JsonElement.GetProperty(string)"/> and returns a string + /// </summary> + /// <param name="element"></param> + /// <param name="propertyName">The name of the property to get the string value of</param> + /// <returns>If the property exists, returns the string stored at that property</returns> + public static string? GetPropString(this in JsonElement element, string propertyName) + { + return element.TryGetProperty(propertyName, out JsonElement el) ? el.GetString() : null; + } + /// <summary> + /// Shortcut extension to <see cref="JsonElement.GetProperty(string)"/> and returns a string + /// </summary> + /// <param name="conf"></param> + /// <param name="propertyName">The name of the property to get the string value of</param> + /// <returns>If the property exists, returns the string stored at that property</returns> + public static string? GetPropString(this IReadOnlyDictionary<string, JsonElement> conf, string propertyName) + { + return conf.TryGetValue(propertyName, out JsonElement el) ? el.GetString() : null; + } + + /// <summary> + /// Shortcut extension to <see cref="JsonElement.GetProperty(string)"/> and returns a string + /// </summary> + /// <param name="conf"></param> + /// <param name="propertyName">The name of the property to get the string value of</param> + /// <returns>If the property exists, returns the string stored at that property</returns> + public static string? GetPropString(this IDictionary<string, JsonElement> conf, string propertyName) + { + return conf.TryGetValue(propertyName, out JsonElement el) ? el.GetString() : null; + } + + /// <summary> + /// Attemts to serialze an object to a JSON encoded string + /// </summary> + /// <param name="obj"></param> + /// <param name="options"><see cref="JsonSerializerOptions"/> to use during serialization</param> + /// <returns>A JSON encoded string of the serialized object, or null if the object is null</returns> + /// <exception cref="NotSupportedException"></exception> + public static string? ToJsonString<T>(this T obj, JsonSerializerOptions? options = null) + { + return obj == null ? null : JsonSerializer.Serialize(obj, options); + } + + /// <summary> + /// Merges the current <see cref="JsonDocument"/> with another <see cref="JsonDocument"/> to + /// create a new document of combined properties + /// </summary> + /// <param name="initial"></param> + /// <param name="other">The <see cref="JsonDocument"/> to combine with the first document</param> + /// <param name="initalName">The name of the new element containing the initial document data</param> + /// <param name="secondName">The name of the new element containing the additional document data</param> + /// <returns>A new document with a parent root containing the combined objects</returns> + public static JsonDocument Merge(this JsonDocument initial, JsonDocument other, string initalName, string secondName) + { + //Open a new memory buffer + using VnMemoryStream ms = new(); + //Encapuslate the memory stream in a writer + using (Utf8JsonWriter writer = new(ms)) + { + //Write the starting + writer.WriteStartObject(); + //Write the first object name + writer.WritePropertyName(initalName); + //Write the inital docuemnt to the stream + initial.WriteTo(writer); + //Write the second object property + writer.WritePropertyName(secondName); + //Write the merging document to the stream + other.WriteTo(writer); + //End the parent element + writer.WriteEndObject(); + } + //rewind the buffer + _ = ms.Seek(0, System.IO.SeekOrigin.Begin); + //Parse the stream into the new document and return it + return JsonDocument.Parse(ms); + } + + /// <summary> + /// Parses a number value into a <see cref="TimeSpan"/> of the specified time + /// </summary> + /// <param name="el"></param> + /// <param name="type">The <see cref="TimeParseType"/> the value represents</param> + /// <returns>The <see cref="TimeSpan"/> of the value</returns> + /// <exception cref="FormatException"></exception> + /// <exception cref="OverflowException"></exception> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="InvalidOperationException"></exception> + public static TimeSpan GetTimeSpan(this in JsonElement el, TimeParseType type) + { + return type switch + { + TimeParseType.Milliseconds => TimeSpan.FromMilliseconds(el.GetDouble()), + TimeParseType.Seconds => TimeSpan.FromSeconds(el.GetDouble()), + TimeParseType.Minutes => TimeSpan.FromMinutes(el.GetDouble()), + TimeParseType.Hours => TimeSpan.FromHours(el.GetDouble()), + TimeParseType.Days => TimeSpan.FromDays(el.GetDouble()), + TimeParseType.Ticks => TimeSpan.FromTicks(el.GetInt64()), + _ => throw new NotSupportedException(), + }; + } + } +}
\ No newline at end of file diff --git a/Utils/src/Extensions/MemoryExtensions.cs b/Utils/src/Extensions/MemoryExtensions.cs new file mode 100644 index 0000000..c8ee5ef --- /dev/null +++ b/Utils/src/Extensions/MemoryExtensions.cs @@ -0,0 +1,769 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: MemoryExtensions.cs +* +* MemoryExtensions.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; +using System.Text; +using System.Buffers; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +namespace VNLib.Utils.Extensions +{ + using Utils.Memory; + using VNLib.Utils.Resources; + + /// <summary> + /// Provides memory based extensions to .NET and VNLib memory abstractions + /// </summary> + public static class MemoryExtensions + { + /// <summary> + /// Rents a new array and stores it as a resource within an <see cref="OpenResourceHandle{T}"/> to return the + /// array when work is completed + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="pool"></param> + /// <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); + } + + /// <summary> + /// Retreives a buffer that is at least the reqested length, and clears the array from 0-size. + /// <br></br> + /// The array may be larger than the requested size, and the entire buffer is zeroed + /// </summary> + /// <param name="pool"></param> + /// <param name="size">The minimum length of the array</param> + /// <param name="zero">True if contents should be zeroed</param> + /// <returns>The zeroed array</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T[] Rent<T>(this ArrayPool<T> pool, int size, bool zero) + { + //Rent the array + T[] arr = pool.Rent(size); + //If zero flag is set, zero only the used section + if (zero) + { + Array.Fill(arr, default); + } + return arr; + } + + /// <summary> + /// Copies the characters within the memory handle to a <see cref="string"/> + /// </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); + } + + /// <summary> + /// Wraps the <see cref="VnTempBuffer{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 VnTempBuffer<T> handle, bool ownsHandle = true) where T : unmanaged + { + _ = handle ?? throw new ArgumentNullException(nameof(handle)); + return new SysBufferMemoryManager<T>(handle, ownsHandle); + } + + /// <summary> + /// Allows direct allocation of a fixed size <see cref="MemoryManager{T}"/> from a <see cref="PrivateHeap"/> instance + /// of the specified number of elements + /// </summary> + /// <typeparam name="T">The unmanaged data type</typeparam> + /// <param name="heap"></param> + /// <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> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryManager<T> DirectAlloc<T>(this IUnmangedHeap heap, ulong size, bool zero = false) where T : unmanaged + { + return new SysBufferMemoryManager<T>(heap, size, zero); + } + + /// <summary> + /// Allows direct allocation of a fixed size <see cref="MemoryManager{T}"/> from a <see cref="PrivateHeap"/> instance + /// of the specified number of elements + /// </summary> + /// <typeparam name="T">The unmanaged data type</typeparam> + /// <param name="heap"></param> + /// <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="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryManager<T> DirectAlloc<T>(this IUnmangedHeap heap, long size, bool zero = false) where T : unmanaged + { + return size < 0 ? throw new ArgumentOutOfRangeException(nameof(size)) : DirectAlloc<T>(heap, (ulong)size, zero); + } + /// <summary> + /// Gets an offset pointer from the base postion to the number of bytes specified. Performs bounds checks + /// </summary> + /// <param name="memory"></param> + /// <param name="elements">Number of elements of type to offset</param> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <returns><typeparamref name="T"/> pointer to the memory offset specified</returns> + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe T* GetOffset<T>(this MemoryHandle<T> memory, long elements) where T : unmanaged + { + return elements < 0 ? throw new ArgumentOutOfRangeException(nameof(elements)) : memory.GetOffset((ulong)elements); + } + /// <summary> + /// Resizes the current handle on the heap + /// </summary> + /// <param name="memory"></param> + /// <param name="elements">Positive number of elemnts the current handle should referrence</param> + /// <exception cref="OverflowException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Resize<T>(this MemoryHandle<T> memory, long elements) where T : unmanaged + { + if (elements < 0) + { + throw new ArgumentOutOfRangeException(nameof(elements)); + } + memory.Resize((ulong)elements); + } + + /// <summary> + /// Resizes the target handle only if the handle is smaller than the requested element count + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="handle"></param> + /// <param name="count">The number of elements to resize to</param> + /// <exception cref="OverflowException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ResizeIfSmaller<T>(this MemoryHandle<T> handle, long count) where T : unmanaged + { + if(count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + ResizeIfSmaller(handle, (ulong)count); + } + + /// <summary> + /// Resizes the target handle only if the handle is smaller than the requested element count + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="handle"></param> + /// <param name="count">The number of elements to resize to</param> + /// <exception cref="OverflowException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ResizeIfSmaller<T>(this MemoryHandle<T> handle, ulong count) where T : unmanaged + { + //Check handle size + if(handle.Length < count) + { + //handle too small, resize + handle.Resize(count); + } + } + +#if TARGET_64_BIT + /// <summary> + /// Gets a 64bit friendly span offset for the current <see cref="MemoryHandle{T}"/> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="block"></param> + /// <param name="offset">The offset (in elements) from the begining of the block</param> + /// <param name="size">The size of the block (in elements)</param> + /// <returns>The offset span</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span<T> GetOffsetSpan<T>(this MemoryHandle<T> block, ulong offset, int size) where T: unmanaged + { + _ = block ?? throw new ArgumentNullException(nameof(block)); + if(size < 0) + { + throw new ArgumentOutOfRangeException(nameof(size)); + } + if(size == 0) + { + return Span<T>.Empty; + } + //Make sure the offset size is within the size of the block + if(offset + (ulong)size <= block.Length) + { + //Get long offset from the destination handle + void* ofPtr = block.GetOffset(offset); + return new Span<T>(ofPtr, size); + } + throw new ArgumentOutOfRangeException(nameof(size)); + } + /// <summary> + /// Gets a 64bit friendly span offset for the current <see cref="MemoryHandle{T}"/> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="block"></param> + /// <param name="offset">The offset (in elements) from the begining of the block</param> + /// <param name="size">The size of the block (in elements)</param> + /// <returns>The offset span</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span<T> GetOffsetSpan<T>(this MemoryHandle<T> block, long offset, int size) where T : unmanaged + { + return offset < 0 ? throw new ArgumentOutOfRangeException(nameof(offset)) : block.GetOffsetSpan<T>((ulong)offset, size); + } + + + /// <summary> + /// Gets a <see cref="SubSequence{T}"/> window within the current block + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="block"></param> + /// <param name="offset">An offset within the handle</param> + /// <param name="size">The size of the window</param> + /// <returns>The new <see cref="SubSequence{T}"/> within the block</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SubSequence<T> GetSubSequence<T>(this MemoryHandle<T> block, ulong offset, int size) where T : unmanaged + { + return new SubSequence<T>(block, offset, size); + } +#else + + /// <summary> + /// Gets a <see cref="SubSequence{T}"/> window within the current block + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="block"></param> + /// <param name="offset">An offset within the handle</param> + /// <param name="size">The size of the window</param> + /// <returns>The new <see cref="SubSequence{T}"/> within the block</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SubSequence<T> GetSubSequence<T>(this MemoryHandle<T> block, int offset, int size) where T : unmanaged + { + return new SubSequence<T>(block, offset, size); + } + + /// <summary> + /// Gets a 64bit friendly span offset for the current <see cref="MemoryHandle{T}"/> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="block"></param> + /// <param name="offset">The offset (in elements) from the begining of the block</param> + /// <param name="size">The size of the block (in elements)</param> + /// <returns>The offset span</returns> + /// <exception cref="OverflowException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span<T> GetOffsetSpan<T>(this MemoryHandle<T> block, long offset, int size) where T : unmanaged + { + //TODO fix 32bit/64 bit, this is a safe lazy workaround + return block.Span.Slice(checked((int) offset), size); + } +#endif + + /// <summary> + /// Wraps the current instance with a <see cref="MemoryPool{T}"/> wrapper + /// to allow System.Memory buffer rentals. + /// </summary> + /// <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) where T : unmanaged + { + return new PrivateBuffersMemoryPool<T>(heap); + } + + /// <summary> + /// Allocates a structure of the specified type on the current unmanged heap and zero's its memory + /// </summary> + /// <typeparam name="T">The structure type</typeparam> + /// <param name="heap"></param> + /// <returns>A pointer to the structure ready for use.</returns> + /// <remarks>Allocations must be freed with <see cref="StructFree{T}(IUnmangedHeap, T*)"/></remarks> + /// <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, (uint)sizeof(T), true); + //returns the handle + return (T*)handle; + } + /// <summary> + /// Frees a structure at the specified address from the this heap. + /// This must be the same heap the structure was allocated from + /// </summary> + /// <typeparam name="T">The structure type</typeparam> + /// <param name="heap"></param> + /// <param name="structPtr">A pointer to the structure</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void StructFree<T>(this IUnmangedHeap heap, T* structPtr) where T : unmanaged + { + IntPtr block = new(structPtr); + //Free block from heap + heap.Free(ref block); + //Clear ref + *structPtr = default; + } + /// <summary> + /// Allocates a block of unmanaged memory of the number of elements to store of an unmanged type + /// </summary> + /// <typeparam name="T">Unmanaged data type to create a block of</typeparam> + /// <param name="heap"></param> + /// <param name="elements">The size of the block (number of elements)</param> + /// <param name="zero">A flag that zeros the allocated block before returned</param> + /// <returns>The unmanaged <see cref="MemoryHandle{T}"/></returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe MemoryHandle<T> Alloc<T>(this IUnmangedHeap heap, ulong elements, bool zero = false) where T : unmanaged + { + //Minimum of one element + elements = Math.Max(elements, 1); + //Get element size + uint elementSize = (uint)sizeof(T); + //If zero flag is set then specify zeroing memory + IntPtr block = heap.Alloc(elements, elementSize, zero); + //Return handle wrapper + return new MemoryHandle<T>(heap, block, elements, zero); + } + /// <summary> + /// Allocates a block of unmanaged memory of the number of elements to store of an unmanged type + /// </summary> + /// <typeparam name="T">Unmanaged data type to create a block of</typeparam> + /// <param name="heap"></param> + /// <param name="elements">The size of the block (number of elements)</param> + /// <param name="zero">A flag that zeros the allocated block before returned</param> + /// <returns>The unmanaged <see cref="MemoryHandle{T}"/></returns> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryHandle<T> Alloc<T>(this IUnmangedHeap heap, long elements, bool zero = false) where T : unmanaged + { + return elements < 0 ? throw new ArgumentOutOfRangeException(nameof(elements)) : Alloc<T>(heap, (ulong)elements, zero); + } + /// <summary> + /// Allocates a buffer from the current heap and initialzies it by copying the initial data buffer + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="heap"></param> + /// <param name="initialData">The initial data to set the buffer to</param> + /// <returns>The initalized <see cref="MemoryHandle{T}"/> block</returns> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryHandle<T> AllocAndCopy<T>(this IUnmangedHeap heap, ReadOnlySpan<T> initialData) where T:unmanaged + { + MemoryHandle<T> handle = heap.Alloc<T>(initialData.Length); + Memory.Copy(initialData, handle, 0); + return handle; + } + + /// <summary> + /// Copies data from the input buffer to the current handle and resizes the handle to the + /// size of the buffer + /// </summary> + /// <typeparam name="T">The unamanged value type</typeparam> + /// <param name="handle"></param> + /// <param name="input">The input buffer to copy data from</param> + /// <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 + { + handle.Resize(input.Length); + Memory.Copy(input, handle, 0); + } + + /// <summary> + /// Allocates a block of unamanged memory of the number of elements of an unmanaged type, and + /// returns the <see cref="UnsafeMemoryHandle{T}"/> that must be used cautiously + /// </summary> + /// <typeparam name="T">The unamanged value type</typeparam> + /// <param name="heap">The heap to allocate block from</param> + /// <param name="elements">The number of elements to allocate</param> + /// <param name="zero">A flag to zero the initial contents of the buffer</param> + /// <returns>The allocated handle of the specified number of elements</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe UnsafeMemoryHandle<T> UnsafeAlloc<T>(this IUnmangedHeap heap, int elements, bool zero = false) where T : unmanaged + { + if (elements < 1) + { + throw new ArgumentException("Elements must be greater than 0", nameof(elements)); + } + //Minimum of one element + elements = Math.Max(elements, 1); + //Get element size + uint elementSize = (uint)sizeof(T); + //If zero flag is set then specify zeroing memory + IntPtr block = heap.Alloc((uint)elements, elementSize, zero); + //handle wrapper + return new (heap, block, elements); + } + + #region VnBufferWriter + + /// <summary> + /// Formats and appends a value type to the writer with proper endianess + /// </summary> + /// <param name="buffer"></param> + /// <param name="value">The value to format and append to the buffer</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static void Append<T>(this ref ForwardOnlyWriter<byte> buffer, T value) where T: unmanaged + { + //Calc size of structure and fix te size of the buffer + int size = Unsafe.SizeOf<T>(); + Span<byte> output = buffer.Remaining[..size]; + + //Format value and write to buffer + MemoryMarshal.Write(output, ref value); + + //If byte order is reversed, reverse elements + if (!BitConverter.IsLittleEndian) + { + output.Reverse(); + } + + //Update written posiion + buffer.Advance(size); + } + + /// <summary> + /// Formats and appends a value type to the writer with proper endianess + /// </summary> + /// <param name="buffer"></param> + /// <param name="value">The value to format and append to the buffer</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static void Append<T>(this ref ForwardOnlyMemoryWriter<byte> buffer, T value) where T : struct + { + //Format value and write to buffer + int size = Unsafe.SizeOf<T>(); + Span<byte> output = buffer.Remaining.Span[..size]; + + //Format value and write to buffer + MemoryMarshal.Write(output, ref value); + + //If byte order is reversed, reverse elements + if (BitConverter.IsLittleEndian) + { + output.Reverse(); + } + + //Update written posiion + buffer.Advance(size); + } + + /// <summary> + /// Formats and appends the value to end of the buffer + /// </summary> + /// <param name="buffer"></param> + /// <param name="value">The value to format and append to the buffer</param> + /// <param name="format">An optional format argument</param> + /// <param name="formatProvider"></param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append<T>(this ref ForwardOnlyWriter<char> buffer, T value, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = default) where T : ISpanFormattable + { + //Format value and write to buffer + if (!value.TryFormat(buffer.Remaining, out int charsWritten, format, formatProvider)) + { + throw new ArgumentOutOfRangeException(nameof(buffer), "The value could not be formatted and appended to the buffer, because there is not enough available space"); + } + //Update written posiion + buffer.Advance(charsWritten); + } + + /// <summary> + /// Formats and appends the value to end of the buffer + /// </summary> + /// <param name="buffer"></param> + /// <param name="value">The value to format and append to the buffer</param> + /// <param name="format">An optional format argument</param> + /// <param name="formatProvider"></param> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append<T>(this ref ForwardOnlyMemoryWriter<char> buffer, T value, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = default) where T : ISpanFormattable + { + //Format value and write to buffer + if (!value.TryFormat(buffer.Remaining.Span, out int charsWritten, format, formatProvider)) + { + throw new ArgumentOutOfRangeException(nameof(buffer), "The value could not be formatted and appended to the buffer, because there is not enough available space"); + } + //Update written posiion + buffer.Advance(charsWritten); + } + + + + /// <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 + /// byte span. A parameter indicates whether to clear the internal state of the + /// encoder after the conversion. + /// </summary> + /// <param name="enc"></param> + /// <param name="chars">Character buffer to encode</param> + /// <param name="offset">The offset in the char buffer to begin encoding chars from</param> + /// <param name="charCount">The number of characers to encode</param> + /// <param name="writer">The buffer writer to use</param> + /// <param name="flush">true to clear the internal state of the encoder after the conversion; otherwise, false.</param> + /// <returns>The actual number of bytes written at the location indicated by the bytes parameter.</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBytes(this Encoder enc, char[] chars, int offset, int charCount, ref ForwardOnlyWriter<byte> writer, bool flush) + { + 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 + /// byte span. A parameter indicates whether to clear the internal state of the + /// encoder after the conversion. + /// </summary> + /// <param name="enc"></param> + /// <param name="chars">The character buffer to encode</param> + /// <param name="writer">The buffer writer to use</param> + /// <param name="flush">true to clear the internal state of the encoder after the conversion; otherwise, false.</param> + /// <returns>The actual number of bytes written at the location indicated by the bytes parameter.</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBytes(this Encoder enc, ReadOnlySpan<char> chars, ref ForwardOnlyWriter<byte> writer, bool flush) + { + //Encode the characters + int written = enc.GetBytes(chars, writer.Remaining, flush); + //Update the writer position + 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 + /// byte span. + /// </summary> + /// <param name="encoding"></param> + /// <param name="chars">The character buffer to encode</param> + /// <param name="writer">The buffer writer to use</param> + /// <returns>The actual number of bytes written at the location indicated by the bytes parameter.</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static int GetBytes(this Encoding encoding, ReadOnlySpan<char> chars, ref ForwardOnlyWriter<byte> writer) + { + //Encode the characters + int written = encoding.GetBytes(chars, writer.Remaining); + //Update the writer position + 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 + /// byte span. + /// </summary> + /// <param name="encoding"></param> + /// <param name="bytes">The binary buffer to decode</param> + /// <param name="writer">The buffer writer to use</param> + /// <returns>The actual number of *characters* written at the location indicated by the chars parameter.</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static int GetChars(this Encoding encoding, ReadOnlySpan<byte> bytes, ref ForwardOnlyWriter<char> writer) + { + int charCount = encoding.GetCharCount(bytes); + //Encode the characters + _ = encoding.GetChars(bytes, writer.Remaining); + //Update the writer position + writer.Advance(charCount); + return charCount; + } + + /// <summary> + /// Converts the buffer data to a <see cref="PrivateString"/> + /// </summary> + /// <returns>A <see cref="PrivateString"/> instance that owns the underlying string memory</returns> + 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> + /// <returns>A <see cref="Span{T}"/> over the modified data</returns> + public static Span<T> AsSpan<T>(this ref ForwardOnlyWriter<T> buffer) => buffer.Buffer[..buffer.Written]; + + + #endregion + + /// <summary> + /// Slices the current array by the specified starting offset to the end + /// of the array + /// </summary> + /// <typeparam name="T">The array type</typeparam> + /// <param name="arr"></param> + /// <param name="start">The start offset of the new array slice</param> + /// <returns>The sliced array</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static T[] Slice<T>(this T[] arr, int start) + { + if(start < 0 || start > arr.Length) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + 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 + /// </summary> + /// <typeparam name="T">The array type</typeparam> + /// <param name="arr"></param> + /// <param name="start">The start offset of the new array slice</param> + /// <param name="count">The size of the new array</param> + /// <returns>The sliced array</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static T[] Slice<T>(this T[] arr, int start, int count) + { + if(start < 0) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + if(count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if(start + count >= arr.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if(count == 0) + { + return Array.Empty<T>(); + } + //Calc the slice range + Range sliceRange = new(start, start + count); + return RuntimeHelpers.GetSubArray(arr, sliceRange); + } + + /// <summary> + /// Creates a new sub-sequence over the target handle. (allows for convient sub span) + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="handle"></param> + /// <param name="start">Intial offset into the handle</param> + /// <returns>The sub-sequence of the current handle</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span<T> AsSpan<T>(this IMemoryHandle<T> handle, int start) => handle.Span[start..]; + + /// <summary> + /// Creates a new sub-sequence over the target handle. (allows for convient sub span) + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="handle"></param> + /// <param name="start">Intial offset into the handle</param> + /// <param name="count">The number of elements within the new sequence</param> + /// <returns>The sub-sequence of the current handle</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span<T> AsSpan<T>(this IMemoryHandle<T> handle, int start, int count) => handle.Span.Slice(start, count); + + /// <summary> + /// Creates a new sub-sequence over the target handle. (allows for convient sub span) + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="handle"></param> + /// <param name="start">Intial offset into the handle</param> + /// <returns>The sub-sequence of the current handle</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span<T> AsSpan<T>(this in UnsafeMemoryHandle<T> handle, int start) where T: unmanaged => handle.Span[start..]; + + /// <summary> + /// Creates a new sub-sequence over the target handle. (allows for convient sub span) + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="handle"></param> + /// <param name="start">Intial offset into the handle</param> + /// <param name="count">The number of elements within the new sequence</param> + /// <returns>The sub-sequence of the current handle</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span<T> AsSpan<T>(this in UnsafeMemoryHandle<T> handle, int start, int count) where T : unmanaged => handle.Span.Slice(start, count); + + /// <summary> + /// Raises an <see cref="ObjectDisposedException"/> if the current handle + /// has been disposed or set as invalid + /// </summary> + /// <param name="handle"></param> + /// <exception cref="ObjectDisposedException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfClosed(this SafeHandle handle) + { + if (handle.IsClosed || handle.IsInvalid) + { + throw new ObjectDisposedException(handle.GetType().Name); + } + } + } +} diff --git a/Utils/src/Extensions/MutexReleaser.cs b/Utils/src/Extensions/MutexReleaser.cs new file mode 100644 index 0000000..84dd60f --- /dev/null +++ b/Utils/src/Extensions/MutexReleaser.cs @@ -0,0 +1,62 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: MutexReleaser.cs +* +* MutexReleaser.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; +using System.Threading; + +namespace VNLib.Utils.Extensions +{ + /// <summary> + /// Represents a releaser handle for a <see cref="Mutex"/> + /// that has been entered and will be released. Best if used + /// within a using() statment + /// </summary> + public readonly struct MutexReleaser : IDisposable, IEquatable<MutexReleaser> + { + private readonly Mutex _mutext; + internal MutexReleaser(Mutex mutex) => _mutext = mutex; + /// <summary> + /// Releases the held System.Threading.Mutex once. + /// </summary> + public readonly void Dispose() => _mutext.ReleaseMutex(); + /// <summary> + /// Releases the held System.Threading.Mutex once. + /// </summary> + public readonly void ReleaseMutext() => _mutext.ReleaseMutex(); + + ///<inheritdoc/> + public bool Equals(MutexReleaser other) => _mutext.Equals(other._mutext); + + ///<inheritdoc/> + public override bool Equals(object? obj) => obj is MutexReleaser releaser && Equals(releaser); + + ///<inheritdoc/> + public override int GetHashCode() => _mutext.GetHashCode(); + + ///<inheritdoc/> + public static bool operator ==(MutexReleaser left, MutexReleaser right) => left.Equals(right); + ///<inheritdoc/> + public static bool operator !=(MutexReleaser left, MutexReleaser right) => !(left == right); + } +}
\ No newline at end of file diff --git a/Utils/src/Extensions/SafeLibraryExtensions.cs b/Utils/src/Extensions/SafeLibraryExtensions.cs new file mode 100644 index 0000000..8866059 --- /dev/null +++ b/Utils/src/Extensions/SafeLibraryExtensions.cs @@ -0,0 +1,103 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: SafeLibraryExtensions.cs +* +* SafeLibraryExtensions.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; +using System.Reflection; + +using VNLib.Utils.Native; + +namespace VNLib.Utils.Extensions +{ + /// <summary> + /// When applied to a delegate, specifies the name of the native method to load + /// </summary> + [AttributeUsage(AttributeTargets.Delegate)] + public sealed class SafeMethodNameAttribute : Attribute + { + /// <summary> + /// Creates a new <see cref="SafeMethodNameAttribute"/> + /// </summary> + /// <param name="MethodName">The name of the native method</param> + public SafeMethodNameAttribute(string MethodName) => this.MethodName = MethodName; + /// <summary> + /// Creates a new <see cref="SafeMethodNameAttribute"/>, that uses the + /// delegate name as the native method name + /// </summary> + public SafeMethodNameAttribute() => MethodName = null; + /// <summary> + /// The name of the native method + /// </summary> + public string? MethodName { get; } + } + + + /// <summary> + /// Contains native library extension methods + /// </summary> + public static class SafeLibraryExtensions + { + const string _missMemberExceptionMessage = $"The delegate type is missing the required {nameof(SafeMethodNameAttribute)} to designate the native method to load"; + + /// <summary> + /// Loads a native method from the current <see cref="SafeLibraryHandle"/> + /// that has a <see cref="SafeMethodNameAttribute"/> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="library"></param> + /// <returns></returns> + /// <exception cref="MissingMemberException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="EntryPointNotFoundException"></exception> + public static SafeMethodHandle<T> GetMethod<T>(this SafeLibraryHandle library) where T : Delegate + { + Type t = typeof(T); + //Get the method name attribute + SafeMethodNameAttribute? attr = t.GetCustomAttribute<SafeMethodNameAttribute>(); + _ = attr ?? throw new MissingMemberException(_missMemberExceptionMessage); + return library.GetMethod<T>(attr.MethodName ?? t.Name); + } + /// <summary> + /// Loads a native method from the current <see cref="SafeLibraryHandle"/> + /// that has a <see cref="SafeMethodNameAttribute"/> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="library"></param> + /// <returns></returns> + /// <exception cref="MissingMemberException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="EntryPointNotFoundException"></exception> + /// <remarks> + /// The libraries handle count is left unmodified + /// </remarks> + public static T DangerousGetMethod<T>(this SafeLibraryHandle library) where T: Delegate + { + Type t = typeof(T); + //Get the method name attribute + SafeMethodNameAttribute? attr = t.GetCustomAttribute<SafeMethodNameAttribute>(); + return string.IsNullOrWhiteSpace(attr?.MethodName) + ? throw new MissingMemberException(_missMemberExceptionMessage) + : library.DangerousGetMethod<T>(attr.MethodName); + } + } +} diff --git a/Utils/src/Extensions/SemSlimReleaser.cs b/Utils/src/Extensions/SemSlimReleaser.cs new file mode 100644 index 0000000..c8a22fe --- /dev/null +++ b/Utils/src/Extensions/SemSlimReleaser.cs @@ -0,0 +1,51 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: SemSlimReleaser.cs +* +* SemSlimReleaser.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; +using System.Threading; + +namespace VNLib.Utils.Extensions +{ + /// <summary> + /// Represents a releaser handle for a <see cref="SemaphoreSlim"/> + /// that has been entered and will be released. Best if used + /// within a using() statment + /// </summary> + public readonly struct SemSlimReleaser : IDisposable + { + private readonly SemaphoreSlim _semaphore; + internal SemSlimReleaser(SemaphoreSlim semaphore) => _semaphore = semaphore; + /// <summary> + /// Releases the System.Threading.SemaphoreSlim object once. + /// </summary> + public readonly void Dispose() => _semaphore.Release(); + /// <summary> + /// Releases the System.Threading.SemaphoreSlim object once. + /// </summary> + /// <returns>The previous count of the <see cref="SemaphoreSlim"/></returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="SemaphoreFullException"></exception> + public readonly int Release() => _semaphore.Release(); + } +}
\ No newline at end of file diff --git a/Utils/src/Extensions/StringExtensions.cs b/Utils/src/Extensions/StringExtensions.cs new file mode 100644 index 0000000..09d6517 --- /dev/null +++ b/Utils/src/Extensions/StringExtensions.cs @@ -0,0 +1,481 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: StringExtensions.cs +* +* StringExtensions.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; +using System.Buffers; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using VNLib.Utils.Memory; + +namespace VNLib.Utils.Extensions +{ + public delegate void StatelessSpanAction(ReadOnlySpan<char> line); + + /// <summary> + /// Extention methods for string (character buffer) + /// </summary> + public static class StringExtensions + { + /// <summary> + /// Split a string based on split value and insert into the specified list + /// </summary> + /// <param name="value"></param> + /// <param name="splitter">The value to split the string on</param> + /// <param name="output">The list to output data to</param> + /// <param name="options">String split options</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split<T>(this string value, string splitter, T output, StringSplitOptions options) where T : ICollection<string> + { + Split(value, splitter.AsSpan(), output, options); + } + /// <summary> + /// Split a string based on split value and insert into the specified list + /// </summary> + /// <param name="value"></param> + /// <param name="splitter">The value to split the string on</param> + /// <param name="output">The list to output data to</param> + /// <param name="options">String split options</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split<T>(this string value, char splitter, T output, StringSplitOptions options) where T: ICollection<string> + { + //Create span from char pointer + ReadOnlySpan<char> cs = MemoryMarshal.CreateReadOnlySpan(ref splitter, 1); + //Call the split function on the span + Split(value, cs, output, options); + } + /// <summary> + /// Split a string based on split value and insert into the specified list + /// </summary> + /// <param name="value"></param> + /// <param name="splitter">The value to split the string on</param> + /// <param name="output">The list to output data to</param> + /// <param name="options">String split options</param> + /// <exception cref="ArgumentNullException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split<T>(this string value, ReadOnlySpan<char> splitter, T output, StringSplitOptions options) where T : ICollection<string> + { + Split(value.AsSpan(), splitter, output, options); + } + /// <summary> + /// Split a string based on split value and insert into the specified list + /// </summary> + /// <param name="value"></param> + /// <param name="splitter">The value to split the string on</param> + /// <param name="output">The list to output data to</param> + /// <param name="options">String split options</param> + /// <exception cref="ArgumentNullException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split<T>(this in ReadOnlySpan<char> value, char splitter, T output, StringSplitOptions options) where T : ICollection<string> + { + //Create span from char pointer + ReadOnlySpan<char> cs = MemoryMarshal.CreateReadOnlySpan(ref splitter, 1); + //Call the split function on the span + Split(in value, cs, output, options); + } + /// <summary> + /// Split a <see cref="ReadOnlySpan{T}"/> based on split value and insert into the specified list + /// </summary> + /// <param name="value"></param> + /// <param name="splitter">The value to split the string on</param> + /// <param name="output">The list to output data to</param> + /// <param name="options">String split options</param> + /// <exception cref="ArgumentNullException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split<T>(this in ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, T output, StringSplitOptions options) where T : ICollection<string> + { + //Create a local function that adds the split strings to the list + static void SplitFound(ReadOnlySpan<char> split, T output) => output.Add(split.ToString()); + //Invoke the split function with the local callback method + Split(in value, splitter, options, SplitFound, output); + } + /// <summary> + /// Split a <see cref="ReadOnlySpan{T}"/> based on split value and pass it to the split delegate handler + /// </summary> + /// <param name="value"></param> + /// <param name="splitter">The sequence to split the string on</param> + /// <param name="options">String split options</param> + /// <param name="splitCb">The action to invoke when a split segment has been found</param> + /// <param name="state">The state to pass to the callback handler</param> + /// <exception cref="ArgumentNullException"></exception> + public static void Split<T>(this in ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, StringSplitOptions options, ReadOnlySpanAction<char, T> splitCb, T state) + { + _ = splitCb ?? throw new ArgumentNullException(nameof(splitCb)); + //Get span over string + ForwardOnlyReader<char> reader = new(value); + //No string options + if (options == 0) + { + do + { + //Find index of the splitter + int start = reader.Window.IndexOf(splitter); + //guard + if (start == -1) + { + break; + } + //Trim and add it regardless of length + splitCb(reader.Window[..start], state); + //shift window + reader.Advance(start + splitter.Length); + } while (true); + //Trim remaining and add it regardless of length + splitCb(reader.Window, state); + } + //Trim but do not remove empties + else if ((options & StringSplitOptions.RemoveEmptyEntries) == 0) + { + do + { + //Find index of the splitter + int start = reader.Window.IndexOf(splitter); + //guard + if (start == -1) + { + break; + } + //Trim and add it regardless of length + splitCb(reader.Window[..start].Trim(), state); + //shift window + reader.Advance(start + splitter.Length); + } while (true); + //Trim remaining and add it regardless of length + splitCb(reader.Window.Trim(), state); + } + //Remove empty entires but do not trim them + else if ((options & StringSplitOptions.TrimEntries) == 0) + { + //Get data before splitter and trim it + ReadOnlySpan<char> data; + do + { + //Find index of the splitter + int start = reader.Window.IndexOf(splitter); + //guard + if (start == -1) + { + break; + } + //Get data before splitter and trim it + data = reader.Window[..start]; + //If its not empty, then add it to the list + if (!data.IsEmpty) + { + splitCb(data, state); + } + //shift window + reader.Advance(start + splitter.Length); + } while (true); + //Add if not empty + if (reader.WindowSize > 0) + { + splitCb(reader.Window, state); + } + } + //Must mean remove and trim + else + { + //Get data before splitter and trim it + ReadOnlySpan<char> data; + do + { + //Find index of the splitter + int start = reader.Window.IndexOf(splitter); + //guard + if (start == -1) + { + break; + } + //Get data before splitter and trim it + data = reader.Window[..start].Trim(); + //If its not empty, then add it to the list + if (!data.IsEmpty) + { + splitCb(data, state); + } + //shift window + reader.Advance(start + splitter.Length); + } while (true); + //Trim remaining + data = reader.Window.Trim(); + //Add if not empty + if (!data.IsEmpty) + { + splitCb(data, state); + } + } + } + + /// <summary> + /// Split a <see cref="ReadOnlySpan{T}"/> based on split value and pass it to the split delegate handler + /// </summary> + /// <param name="value"></param> + /// <param name="splitter">The character to split the string on</param> + /// <param name="options">String split options</param> + /// <param name="splitCb">The action to invoke when a split segment has been found</param> + /// <param name="state"></param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split<T>(this in ReadOnlySpan<char> value, char splitter, StringSplitOptions options, ReadOnlySpanAction<char, T> splitCb, T state) + { + //Alloc a span for char + ReadOnlySpan<char> cs = MemoryMarshal.CreateReadOnlySpan(ref splitter, 1); + //Call the split function on the span + Split(in value, cs, options, splitCb, state); + } + /// <summary> + /// Split a <see cref="ReadOnlySpan{T}"/> based on split value and pass it to the split delegate handler + /// </summary> + /// <param name="value"></param> + /// <param name="splitter">The sequence to split the string on</param> + /// <param name="options">String split options</param> + /// <param name="splitCb">The action to invoke when a split segment has been found</param> + /// <exception cref="ArgumentNullException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split(this in ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, StringSplitOptions options, StatelessSpanAction splitCb) + { + //Create a SpanSplitDelegate with the non-typed delegate as the state argument + static void ssplitcb(ReadOnlySpan<char> param, StatelessSpanAction callback) => callback(param); + //Call split with the new callback delegate + Split(in value, splitter, options, ssplitcb, splitCb); + } + /// <summary> + /// Split a <see cref="ReadOnlySpan{T}"/> based on split value and pass it to the split delegate handler + /// </summary> + /// <param name="value"></param> + /// <param name="splitter">The character to split the string on</param> + /// <param name="options">String split options</param> + /// <param name="splitCb">The action to invoke when a split segment has been found</param> + /// <exception cref="ArgumentNullException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split(this in ReadOnlySpan<char> value, char splitter, StringSplitOptions options, StatelessSpanAction splitCb) + { + //Create a SpanSplitDelegate with the non-typed delegate as the state argument + static void ssplitcb(ReadOnlySpan<char> param, StatelessSpanAction callback) => callback(param); + //Call split with the new callback delegate + Split(in value, splitter, options, ssplitcb, splitCb); + } + + /// <summary> + /// Gets the index of the end of the found sequence + /// </summary> + /// <param name="data"></param> + /// <param name="search">Sequence to search for within the current sequence</param> + /// <returns>the index of the end of the sequenc</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int EndOf(this in ReadOnlySpan<char> data, ReadOnlySpan<char> search) + { + int index = data.IndexOf(search); + return index > -1 ? index + search.Length : -1; + } + /// <summary> + /// Gets the index of the end of the found character + /// </summary> + /// <param name="data"></param> + /// <param name="search">Character to search for within the current sequence</param> + /// <returns>the index of the end of the sequence</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int EndOf(this in ReadOnlySpan<char> data, char search) + { + int index = data.IndexOf(search); + return index > -1 ? index + 1 : -1; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOf(this in Memory<byte> data, byte search) => data.Span.IndexOf(search); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOf(this in Memory<byte> data, ReadOnlySpan<byte> search) => data.Span.IndexOf(search); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOf(this in Memory<byte> data, ReadOnlyMemory<byte> search) => IndexOf(data, search.Span); + + /// <summary> + /// Slices the current span from the begining of the segment to the first occurrance of the specified character. + /// If the character is not found, the entire segment is returned + /// </summary> + /// <param name="data"></param> + /// <param name="search">The delimiting character</param> + /// <returns>The segment of data before the search character, or the entire segment if not found</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan<char> SliceBeforeParam(this in ReadOnlySpan<char> data, char search) + { + //Find the index of the specified data + int index = data.IndexOf(search); + //Return the slice of data before the index, or an empty span if it was not found + return index > -1 ? data[..index] : data; + } + /// <summary> + /// Slices the current span from the begining of the segment to the first occurrance of the specified character sequence. + /// If the character sequence is not found, the entire segment is returned + /// </summary> + /// <param name="data"></param> + /// <param name="search">The delimiting character sequence</param> + /// <returns>The segment of data before the search character, or the entire <paramref name="data"/> if the seach sequence is not found</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan<char> SliceBeforeParam(this in ReadOnlySpan<char> data, ReadOnlySpan<char> search) + { + //Find the index of the specified data + int index = data.IndexOf(search); + //Return the slice of data before the index, or an empty span if it was not found + return index > -1 ? data[..index] : data; + } + /// <summary> + /// Gets the remaining segment of data after the specified search character or <see cref="ReadOnlySpan{T}.Empty"/> + /// if the search character is not found within the current segment + /// </summary> + /// <param name="data"></param> + /// <param name="search">The character to search for within the segment</param> + /// <returns>The segment of data after the search character or <see cref="ReadOnlySpan{T}.Empty"/> if not found</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan<char> SliceAfterParam(this in ReadOnlySpan<char> data, char search) + { + //Find the index of the specified data + int index = EndOf(in data, search); + //Return the slice of data after the index, or an empty span if it was not found + return index > -1 ? data[index..] : ReadOnlySpan<char>.Empty; + } + /// <summary> + /// Gets the remaining segment of data after the specified search sequence or <see cref="ReadOnlySpan{T}.Empty"/> + /// if the search sequence is not found within the current segment + /// </summary> + /// <param name="data"></param> + /// <param name="search">The sequence to search for within the segment</param> + /// <returns>The segment of data after the search sequence or <see cref="ReadOnlySpan{T}.Empty"/> if not found</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan<char> SliceAfterParam(this in ReadOnlySpan<char> data, ReadOnlySpan<char> search) + { + //Find the index of the specified data + int index = EndOf(data, search); + //Return the slice of data after the index, or an empty span if it was not found + return index > -1 ? data[index..] : ReadOnlySpan<char>.Empty; + } + /// <summary> + /// Trims any leading or trailing <c>'\r'|'\n'|' '</c>(whitespace) characters from the segment + /// </summary> + /// <returns>The trimmed segment</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan<char> TrimCRLF(this in ReadOnlySpan<char> data) + { + int start = 0, end = data.Length; + //trim leading \r\n chars + while(start < end) + { + char t = data[start]; + //If character \r or \n slice it off + if (t != '\r' && t != '\n' && t != ' ') { + break; + } + //Shift + start++; + } + //remove trailing crlf characters + while (end > start) + { + char t = data[end - 1]; + //If character \r or \n slice it off + if (t != '\r' && t != '\n' && t != ' ') { + break; + } + end--; + } + return data[start..end]; + } + + /// <summary> + /// Replaces a character sequence within the buffer + /// </summary> + /// <param name="buffer">The character buffer to process</param> + /// <param name="search">The sequence to search for</param> + /// <param name="replace">The sequence to write in the place of the search parameter</param> + /// <exception cref="OutOfMemoryException"></exception> + public static int Replace(this ref Span<char> buffer, ReadOnlySpan<char> search, ReadOnlySpan<char> replace) + { + ForwardOnlyWriter<char> writer = new (buffer); + writer.Replace(search, replace); + return writer.Written; + } + + /// <summary> + /// Replaces a character sequence within the writer + /// </summary> + /// <param name="writer"></param> + /// <param name="search">The sequence to search for</param> + /// <param name="replace">The sequence to write in the place of the search parameter</param> + /// <exception cref="OutOfMemoryException"></exception> + public static void Replace(this ref ForwardOnlyWriter<char> writer, ReadOnlySpan<char> search, ReadOnlySpan<char> replace) + { + Span<char> buffer = writer.AsSpan(); + //If the search and replacment parameters are the same length + if (search.Length == replace.Length) + { + buffer.ReplaceInPlace(search, replace); + return; + } + //Search and replace are not the same length + int searchLen = search.Length, start = buffer.IndexOf(search); + if(start == -1) + { + return; + } + //Replacment might be empty + writer.Reset(); + do + { + //Append the data before the split character + writer.Append(buffer[..start]); + //Append the replacment + writer.Append(replace); + //Shift buffer to the end of the + buffer = buffer[(start + searchLen)..]; + //search for next index + start = buffer.IndexOf(search); + } while (start > -1); + //Write remaining data + writer.Append(replace); + } + /// <summary> + /// Replaces very ocurrance of character sequence within a buffer with another sequence of the same length + /// </summary> + /// <param name="buffer"></param> + /// <param name="search">The sequence to search for</param> + /// <param name="replace">The sequence to replace the found sequence with</param> + /// <exception cref="ArgumentException"></exception> + public static void ReplaceInPlace(this Span<char> buffer, ReadOnlySpan<char> search, ReadOnlySpan<char> replace) + { + if(search.Length != replace.Length) + { + throw new ArgumentException("Search parameter and replacment parameter must be the same length"); + } + int start = buffer.IndexOf(search); + while(start > -1) + { + //Shift the buffer to the begining of the search parameter + buffer = buffer[start..]; + //Overwite the search parameter + replace.CopyTo(buffer); + //Search for next index of the search character + start = buffer.IndexOf(search); + } + } + } +}
\ No newline at end of file diff --git a/Utils/src/Extensions/ThreadingExtensions.cs b/Utils/src/Extensions/ThreadingExtensions.cs new file mode 100644 index 0000000..cc9fab9 --- /dev/null +++ b/Utils/src/Extensions/ThreadingExtensions.cs @@ -0,0 +1,226 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ThreadingExtensions.cs +* +* ThreadingExtensions.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; +using System.Threading; +using System.Threading.Tasks; +using VNLib.Utils.Resources; + +namespace VNLib.Utils.Extensions +{ + + /// <summary> + /// Provides extension methods to common threading and TPL library operations + /// </summary> + public static class ThreadingExtensions + { + /// <summary> + /// Allows an <see cref="OpenResourceHandle{TResource}"/> to execute within a scope limited context + /// </summary> + /// <typeparam name="TResource">The resource type</typeparam> + /// <param name="rh"></param> + /// <param name="safeCallback">The function body that will execute with controlled access to the resource</param> + public static void EnterSafeContext<TResource>(this OpenResourceHandle<TResource> rh, Action<TResource> safeCallback) + { + using (rh) + { + safeCallback(rh.Resource); + } + } + + /// <summary> + /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/> while observing a <see cref="CancellationToken"/> + /// and getting a releaser handle + /// </summary> + /// <param name="semaphore"></param> + /// <param name="cancellationToken">A token to cancel the operation</param> + /// <returns>A releaser handle that may be disposed to release the semaphore</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="OperationCanceledException"></exception> + public static async Task<SemSlimReleaser> GetReleaserAsync(this SemaphoreSlim semaphore, CancellationToken cancellationToken = default) + { + await semaphore.WaitAsync(cancellationToken); + return new SemSlimReleaser(semaphore); + } + /// <summary> + /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/> using a 32-bit signed integer to measure the time intervale + /// and getting a releaser handle + /// </summary> + /// <param name="semaphore"></param> + /// <param name="timeout">A the maximum amount of time in milliseconds to wait to enter the semaphore</param> + /// <returns>A releaser handle that may be disposed to release the semaphore</returns> + /// <exception cref="TimeoutException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public static async Task<SemSlimReleaser> GetReleaserAsync(this SemaphoreSlim semaphore, int timeout) + { + if (await semaphore.WaitAsync(timeout)) + { + return new SemSlimReleaser(semaphore); + } + throw new TimeoutException("Failed to enter the semaphore before the specified timeout period"); + } + + /// <summary> + /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/> + /// </summary> + /// <param name="semaphore"></param> + /// <returns>A releaser handler that releases the semaphore when disposed</returns> + /// <exception cref="ObjectDisposedException"></exception> + public static SemSlimReleaser GetReleaser(this SemaphoreSlim semaphore) + { + semaphore.Wait(); + return new SemSlimReleaser(semaphore); + } + /// <summary> + /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/> + /// </summary> + /// <param name="semaphore"></param> + /// <param name="timeout">A the maximum amount of time in milliseconds to wait to enter the semaphore</param> + /// <returns>A releaser handler that releases the semaphore when disposed</returns> + /// <exception cref="TimeoutException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public static SemSlimReleaser GetReleaser(this SemaphoreSlim semaphore, int timeout) + { + if (semaphore.Wait(timeout)) + { + return new SemSlimReleaser(semaphore); + } + throw new TimeoutException("Failed to enter the semaphore before the specified timeout period"); + } + + /// <summary> + /// Blocks the current thread until it can enter the <see cref="Mutex"/> + /// </summary> + /// <param name="mutex"></param> + /// <returns>A releaser handler that releases the semaphore when disposed</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="AbandonedMutexException"></exception> + public static MutexReleaser Enter(this Mutex mutex) + { + mutex.WaitOne(); + return new MutexReleaser(mutex); + } + /// <summary> + /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/> + /// </summary> + /// <param name="mutex"></param> + /// <param name="timeout">A the maximum amount of time in milliseconds to wait to enter the semaphore</param> + /// <returns>A releaser handler that releases the semaphore when disposed</returns> + /// <exception cref="TimeoutException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public static MutexReleaser Enter(this Mutex mutex, int timeout) + { + if (mutex.WaitOne(timeout)) + { + return new MutexReleaser(mutex); + } + throw new TimeoutException("Failed to enter the semaphore before the specified timeout period"); + } + + private static readonly Task<bool> TrueCompleted = Task.FromResult(true); + private static readonly Task<bool> FalseCompleted = Task.FromResult(false); + + /// <summary> + /// Asynchronously waits for a the <see cref="WaitHandle"/> to receive a signal. This method spins until + /// a thread yield will occur, then asynchronously yields. + /// </summary> + /// <param name="handle"></param> + /// <param name="timeoutMs">The timeout interval in milliseconds</param> + /// <returns> + /// A task that compeletes when the wait handle receives a signal or times-out, + /// the result of the awaited task will be <c>true</c> if the signal is received, or + /// <c>false</c> if the timeout interval expires + /// </returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static Task<bool> WaitAsync(this WaitHandle handle, int timeoutMs = Timeout.Infinite) + { + _ = handle ?? throw new ArgumentNullException(nameof(handle)); + //test non-blocking handle state + if (handle.WaitOne(0)) + { + return TrueCompleted; + } + //When timeout is 0, wh will block, return false + else if(timeoutMs == 0) + { + return FalseCompleted; + } + //Init short lived spinwait + SpinWait sw = new(); + //Spin until yield occurs + while (!sw.NextSpinWillYield) + { + sw.SpinOnce(); + //Check handle state + if (handle.WaitOne(0)) + { + return TrueCompleted; + } + } + //Completion source used to signal the awaiter when the wait handle is signaled + TaskCompletionSource<bool> completion = new(TaskCreationOptions.None); + //Register wait on threadpool to complete the task source + RegisteredWaitHandle registration = ThreadPool.RegisterWaitForSingleObject(handle, TaskCompletionCallback, completion, timeoutMs, true); + //Register continuation to cleanup + _ = completion.Task.ContinueWith(CleanupContinuation, registration, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default) + .ConfigureAwait(false); + return completion.Task; + } + + private static void CleanupContinuation(Task<bool> task, object? taskCompletion) + { + RegisteredWaitHandle registration = (taskCompletion as RegisteredWaitHandle)!; + registration.Unregister(null); + task.Dispose(); + } + private static void TaskCompletionCallback(object? tcsState, bool timedOut) + { + TaskCompletionSource<bool> completion = (tcsState as TaskCompletionSource<bool>)!; + //Set the result of the wait handle timeout + _ = completion.TrySetResult(!timedOut); + } + + + /// <summary> + /// Registers a callback method that will be called when the token has been cancelled. + /// This method waits indefinitely for the token to be cancelled. + /// </summary> + /// <param name="token"></param> + /// <param name="callback">The callback method to invoke when the token has been cancelled</param> + /// <returns>A task that may be unobserved, that completes when the token has been cancelled</returns> + public static Task RegisterUnobserved(this CancellationToken token, Action callback) + { + //Call callback when the wait handle is set + return token.WaitHandle.WaitAsync() + .ContinueWith(static (t, callback) => (callback as Action)!.Invoke(), + callback, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default + ); + } + } +}
\ No newline at end of file diff --git a/Utils/src/Extensions/TimerExtensions.cs b/Utils/src/Extensions/TimerExtensions.cs new file mode 100644 index 0000000..a980d63 --- /dev/null +++ b/Utils/src/Extensions/TimerExtensions.cs @@ -0,0 +1,66 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: TimerExtensions.cs +* +* TimerExtensions.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; +using System.Threading; +using VNLib.Utils.Resources; + +namespace VNLib.Utils.Extensions +{ + public static class TimerExtensions + { + /// <summary> + /// Attempts to stop the timer + /// </summary> + /// <returns>True if the timer was successfully modified, false otherwise</returns> + public static bool Stop(this Timer timer) => timer.Change(Timeout.Infinite, Timeout.Infinite); + + /// <summary> + /// Attempts to stop an active timer and prepare a <see cref="OpenHandle"/> configured to restore the state of the timer to the specified timespan + /// </summary> + /// <param name="timer"></param> + /// <param name="resumeTime"><see cref="TimeSpan"/> representing the amount of time the timer should wait before invoking the callback function</param> + /// <returns>A new <see cref="OpenHandle"/> if the timer was stopped successfully that will resume the timer when closed, null otherwise</returns> + public static OpenHandle Stop(this Timer timer, TimeSpan resumeTime) + { + return timer.Change(Timeout.Infinite, Timeout.Infinite) ? new CallbackOpenHandle(() => timer.Change(resumeTime, Timeout.InfiniteTimeSpan)) : null; + } + + /// <summary> + /// Attempts to reset and start a timer + /// </summary> + /// <param name="timer"></param> + /// <param name="wait"><see cref="TimeSpan"/> to wait before the timer event is fired</param> + /// <returns>True if the timer was successfully modified</returns> + public static bool Restart(this Timer timer, TimeSpan wait) => timer.Change(wait, Timeout.InfiniteTimeSpan); + + /// <summary> + /// Attempts to reset and start a timer + /// </summary> + /// <param name="timer"></param> + /// <param name="waitMilliseconds">Time in milliseconds to wait before the timer event is fired</param> + /// <returns>True if the timer was successfully modified</returns> + public static bool Restart(this Timer timer, int waitMilliseconds) => timer.Change(waitMilliseconds, Timeout.Infinite); + } +}
\ No newline at end of file diff --git a/Utils/src/Extensions/VnStringExtensions.cs b/Utils/src/Extensions/VnStringExtensions.cs new file mode 100644 index 0000000..285fc4f --- /dev/null +++ b/Utils/src/Extensions/VnStringExtensions.cs @@ -0,0 +1,418 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnStringExtensions.cs +* +* VnStringExtensions.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; +using System.Linq; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Utils.Memory; + +namespace VNLib.Utils.Extensions +{ + [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "<Pending>")] + public static class VnStringExtensions + { + /// <summary> + /// Derermines if the character exists within the instance + /// </summary> + /// <param name="str"></param> + /// <param name="value">The value to find</param> + /// <returns>True if the character exists within the instance</returns> + /// <exception cref="ObjectDisposedException"></exception> + public static bool Contains(this VnString str, char value) => str.AsSpan().Contains(value); + /// <summary> + /// Derermines if the sequence exists within the instance + /// </summary> + /// <param name="str"></param> + /// <param name="value">The sequence to find</param> + /// <param name="stringComparison"></param> + /// <returns>True if the character exists within the instance</returns> + /// <exception cref="ObjectDisposedException"></exception> + + public static bool Contains(this VnString str, ReadOnlySpan<char> value, StringComparison stringComparison) => str.AsSpan().Contains(value, stringComparison); + + /// <summary> + /// Searches for the first occurrance of the specified character within the current instance + /// </summary> + /// <param name="str"></param> + /// <param name="value">The character to search for within the instance</param> + /// <returns>The 0 based index of the occurance, -1 if the character was not found</returns> + /// <exception cref="ObjectDisposedException"></exception> + public static int IndexOf(this VnString str, char value) => str.IsEmpty ? -1 : str.AsSpan().IndexOf(value); + /// <summary> + /// Searches for the first occurrance of the specified sequence within the current instance + /// </summary> + /// <param name="str"></param> + /// <param name="search">The sequence to search for</param> + /// <returns>The 0 based index of the occurance, -1 if the sequence was not found</returns> + /// <exception cref="ObjectDisposedException"></exception> + public static int IndexOf(this VnString str, ReadOnlySpan<char> search) + { + //Using spans to avoid memory leaks... + ReadOnlySpan<char> self = str.AsSpan(); + return self.IndexOf(search); + } + /// <summary> + /// Searches for the first occurrance of the specified sequence within the current instance + /// </summary> + /// <param name="str"></param> + /// <param name="search">The sequence to search for</param> + /// <param name="comparison">The <see cref="StringComparison"/> type to use in searchr</param> + /// <returns>The 0 based index of the occurance, -1 if the sequence was not found</returns> + /// <exception cref="ObjectDisposedException"></exception> + public static int IndexOf(this VnString str, ReadOnlySpan<char> search, StringComparison comparison) + { + //Using spans to avoid memory leaks... + ReadOnlySpan<char> self = str.AsSpan(); + return self.IndexOf(search, comparison); + } + /// <summary> + /// Searches for the 0 based index of the first occurance of the search parameter after the start index. + /// </summary> + /// <param name="str"></param> + /// <param name="search">The sequence of data to search for</param> + /// <param name="start">The lower boundry of the search area</param> + /// <returns>The absolute index of the first occurrance within the instance, -1 if the sequency was not found in the windowed segment</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public static int IndexOf(this VnString str, ReadOnlySpan<char> search, int start) + { + if (start < 0) + { + throw new ArgumentOutOfRangeException(nameof(start), "Start cannot be less than 0"); + } + //Get shifted window + ReadOnlySpan<char> self = str.AsSpan()[start..]; + //Check indexof + int index = self.IndexOf(search); + return index > -1 ? index + start : -1; + } + + /// <summary> + /// Returns the realtive index after the specified sequence within the <see cref="VnString"/> instance + /// </summary> + /// <param name="str"></param> + /// <param name="search">The sequence to search for</param> + /// <returns>The index after the found sequence within the string, -1 if the sequence was not found within the instance</returns> + /// <exception cref="ObjectDisposedException"></exception> + public static int EndOf(this VnString str, ReadOnlySpan<char> search) + { + //Try to get the index of the data + int index = IndexOf(str, search); + //If the data was found, add the length to get the end of the string + return index > -1 ? index + search.Length : -1; + } + + /// <summary> + /// Allows for trimming whitespace characters in a realtive sequence from + /// within a <see cref="VnString"/> buffer defined by the start and end parameters + /// and returning the trimmed entry. + /// </summary> + /// <param name="data"></param> + /// <param name="start">The starting position within the sequence to trim</param> + /// <param name="end">The end of the sequence to trim</param> + /// <returns>The trimmed <see cref="VnString"/> instance as a child of the original entry</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="IndexOutOfRangeException"></exception> + public static VnString AbsoluteTrim(this VnString data, int start, int end) + { + AbsoluteTrim(data, ref start, ref end); + return data[start..end]; + } + /// <summary> + /// Finds whitespace characters within the sequence defined between start and end parameters + /// and adjusts the specified window to "trim" whitespace + /// </summary> + /// <param name="data"></param> + /// <param name="start">The starting position within the sequence to trim</param> + /// <param name="end">The end of the sequence to trim</param> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="IndexOutOfRangeException"></exception> + public static void AbsoluteTrim(this VnString data, ref int start, ref int end) + { + ReadOnlySpan<char> trimmed = data.AsSpan(); + //trim leading whitespace + while (start < end) + { + //If whitespace character shift start up + if (trimmed[start] != ' ') + { + break; + } + //Shift + start++; + } + //remove trailing whitespace characters + while (end > start) + { + //If whiterspace character shift end param down + if (trimmed[end - 1] != ' ') + { + break; + } + end--; + } + } + /// <summary> + /// Allows for trimming whitespace characters in a realtive sequence from + /// within a <see cref="VnString"/> buffer and returning the trimmed entry. + /// </summary> + /// <param name="data"></param> + /// <param name="start">The starting position within the sequence to trim</param> + /// <returns>The trimmed <see cref="VnString"/> instance as a child of the original entry</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="IndexOutOfRangeException"></exception> + public static VnString AbsoluteTrim(this VnString data, int start) => AbsoluteTrim(data, start, data.Length); + /// <summary> + /// Trims leading or trailing whitespace characters and returns a new child instance + /// without leading or trailing whitespace + /// </summary> + /// <returns>A child <see cref="VnString"/> of the current instance without leading or trailing whitespaced</returns> + /// <exception cref="ObjectDisposedException"></exception> + public static VnString RelativeTirm(this VnString data) => AbsoluteTrim(data, 0); + + /// <summary> + /// Allows for enumeration of segments of data within the specified <see cref="VnString"/> instance that are + /// split by the search parameter + /// </summary> + /// <param name="data"></param> + /// <param name="search">The sequence of data to delimit segments</param> + /// <param name="options">The options used to split the string instances</param> + /// <returns>An iterator to enumerate the split segments</returns> + /// <exception cref="ObjectDisposedException"></exception> + public static IEnumerable<VnString> Split(this VnString data, ReadOnlyMemory<char> search, StringSplitOptions options = StringSplitOptions.None) + { + int lowerBound = 0; + //Make sure the length of the search param is not 0 + if(search.IsEmpty) + { + //Return the entire string + yield return data; + } + //No string options + else if (options == 0) + { + do + { + //Capture the first = and store argument + value + int splitIndex = data.IndexOf(search.Span, lowerBound); + //If no split index is found, then return remaining data + if (splitIndex == -1) + { + break; + } + yield return data[lowerBound..splitIndex]; + //Shift the lower window to the end of the last string + lowerBound = splitIndex + search.Length; + } while (true); + //Return remaining data + yield return data[lowerBound..]; + } + //Trim but do not remove empties + else if ((options & StringSplitOptions.RemoveEmptyEntries) == 0) + { + do + { + //Capture the first = and store argument + value + int splitIndex = data.IndexOf(search.Span, lowerBound); + //If no split index is found, then return remaining data + if (splitIndex == -1) + { + break; + } + //trim and return + yield return data.AbsoluteTrim(lowerBound, splitIndex); + //Shift the lower window to the end of the last string + lowerBound = splitIndex + search.Length; + } while (true); + //Return remaining data + yield return data.AbsoluteTrim(lowerBound); + } + //Remove empty entires but do not trim them + else if ((options & StringSplitOptions.TrimEntries) == 0) + { + do + { + //Capture the first = and store argument + value + int splitIndex = data.IndexOf(search.Span, lowerBound); + //If no split index is found, then return remaining data + if (splitIndex == -1) + { + break; + } + //If the split index is the next sequence, then the result is empty, so exclude it + else if(splitIndex > 0) + { + yield return data[lowerBound..splitIndex]; + } + //Shift the lower window to the end of the last string + lowerBound = splitIndex + search.Length; + } while (true); + //Return remaining data if available + if (lowerBound < data.Length) + { + yield return data[lowerBound..]; + } + } + //Must mean remove and trim + else + { + //Get stack varables to pass to trim function + int trimStart, trimEnd; + do + { + //Capture the first = and store argument + value + int splitIndex = data.IndexOf(search.Span, lowerBound); + //If no split index is found, then return remaining data + if (splitIndex == -1) + { + break; + } + //Get stack varables to pass to trim function + trimStart = lowerBound; + trimEnd = splitIndex; //End of the segment is the relative split index + the lower bound of the window + //Trim whitespace chars + data.AbsoluteTrim(ref trimStart, ref trimEnd); + //See if the string has data + if((trimEnd - trimStart) > 0) + { + yield return data[trimStart..trimEnd]; + } + //Shift the lower window to the end of the last string + lowerBound = splitIndex + search.Length; + } while (true); + //Trim remaining + trimStart = lowerBound; + trimEnd = data.Length; + data.AbsoluteTrim(ref trimStart, ref trimEnd); + //If the remaining string is not empty return it + if ((trimEnd - trimStart) > 0) + { + yield return data[trimStart..trimEnd]; + } + } + } + + /// <summary> + /// Trims any leading or trailing <c>'\r'|'\n'|' '</c>(whitespace) characters from the segment + /// </summary> + /// <returns>The trimmed segment</returns> + /// <exception cref="ObjectDisposedException"></exception> + public static VnString TrimCRLF(this VnString data) + { + ReadOnlySpan<char> trimmed = data.AsSpan(); + int start = 0, end = trimmed.Length; + //trim leading \r\n chars + while (start < end) + { + char t = trimmed[start]; + //If character \r or \n slice it off + if (t != '\r' && t != '\n' && t != ' ') { + break; + } + //Shift + start++; + } + //remove trailing crlf characters + while (end > start) + { + char t = trimmed[end - 1]; + //If character \r or \n slice it off + if (t != '\r' && t != '\n' && t != ' ') { + break; + } + end--; + } + return data[start..end]; + } + + /// <summary> + /// Unoptimized character enumerator. You should use <see cref="VnString.AsSpan"/> to enumerate the unerlying data. + /// </summary> + /// <returns>The next character in the sequence</returns> + /// <exception cref="ObjectDisposedException"></exception> + public static IEnumerator<char> GetEnumerator(this VnString data) + { + int index = 0; + while (index < data.Length) + { + yield return data[index++]; + } + } + /// <summary> + /// Converts the current handle to a <see cref="VnString"/>, a zero-alloc immutable wrapper + /// for a memory handle + /// </summary> + /// <param name="handle"></param> + /// <param name="length">The number of characters from the handle to reference (length of the string)</param> + /// <returns>The new <see cref="VnString"/> wrapper</returns> + /// <exception cref="OverflowException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static VnString ToVnString(this MemoryHandle<char> handle, int length) + { + if(handle.Length > int.MaxValue) + { + throw new OverflowException("The handle is larger than 2GB in size"); + } + return VnString.ConsumeHandle(handle, 0, length); + } + /// <summary> + /// Converts the current handle to a <see cref="VnString"/>, a zero-alloc immutable wrapper + /// for a memory handle + /// </summary> + /// <param name="handle"></param> + /// <returns>The new <see cref="VnString"/> wrapper</returns> + /// <exception cref="OverflowException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static VnString ToVnString(this MemoryHandle<char> handle) + { + return VnString.ConsumeHandle(handle, 0, handle.IntLength); + } + /// <summary> + /// Converts the current handle to a <see cref="VnString"/>, a zero-alloc immutable wrapper + /// for a memory handle + /// </summary> + /// <param name="handle"></param> + /// <param name="offset">The offset in characters that represents the begining of the string</param> + /// <param name="length">The number of characters from the handle to reference (length of the string)</param> + /// <returns>The new <see cref="VnString"/> wrapper</returns> + /// <exception cref="OverflowException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static VnString ToVnString(this MemoryHandle<char> handle, +#if TARGET_64_BIT + ulong offset, +#else + int offset, +#endif + int length) + { + if (handle.Length > int.MaxValue) + { + throw new OverflowException("The handle is larger than 2GB in size"); + } + return VnString.ConsumeHandle(handle, offset, length); + } + } +}
\ No newline at end of file diff --git a/Utils/src/IIndexable.cs b/Utils/src/IIndexable.cs new file mode 100644 index 0000000..129d703 --- /dev/null +++ b/Utils/src/IIndexable.cs @@ -0,0 +1,43 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IIndexable.cs +* +* IIndexable.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 +{ + /// <summary> + /// Provides an interface that provides an indexer + /// </summary> + /// <typeparam name="TKey">The lookup Key</typeparam> + /// <typeparam name="TValue">The lookup value</typeparam> + public interface IIndexable<TKey, TValue> + { + /// <summary> + /// Gets or sets the value at the specified index in the collection + /// </summary> + /// <param name="key">The key to lookup the value at</param> + /// <returns>The value at the specified key</returns> + TValue this[TKey key] { get; set;} + } +} diff --git a/Utils/src/IO/ArrayPoolStreamBuffer.cs b/Utils/src/IO/ArrayPoolStreamBuffer.cs new file mode 100644 index 0000000..df366e3 --- /dev/null +++ b/Utils/src/IO/ArrayPoolStreamBuffer.cs @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ArrayPoolStreamBuffer.cs +* +* ArrayPoolStreamBuffer.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; +using System.Buffers; + +namespace VNLib.Utils.IO +{ + internal class ArrayPoolStreamBuffer<T> : ISlindingWindowBuffer<T> + { + private readonly ArrayPool<T> _pool; + private T[] _buffer; + + public ArrayPoolStreamBuffer(ArrayPool<T> pool, int bufferSize) + { + _pool = pool; + _buffer = _pool.Rent(bufferSize); + } + + public int WindowStartPos { get; set; } + public int WindowEndPos { get; set; } + + public Memory<T> Buffer => _buffer.AsMemory(); + + public void Advance(int count) + { + WindowEndPos += count; + } + + public void AdvanceStart(int count) + { + WindowStartPos += count; + } + + public void Close() + { + //Return buffer to pool + _pool.Return(_buffer); + _buffer = null; + } + + public void Reset() + { + //Reset window positions + WindowStartPos = 0; + WindowEndPos = 0; + } + } +}
\ No newline at end of file diff --git a/Utils/src/IO/BackingStream.cs b/Utils/src/IO/BackingStream.cs new file mode 100644 index 0000000..cb56b09 --- /dev/null +++ b/Utils/src/IO/BackingStream.cs @@ -0,0 +1,181 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: BackingStream.cs +* +* BackingStream.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; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Provides basic stream support sync/async stream operations to a + /// backing stream with virtual event methods. Provides a pass-through + /// as best as possbile. + /// </summary> + public abstract class BackingStream<T> : Stream where T: Stream + { + /// <summary> + /// The backing/underlying stream operations are being performed on + /// </summary> + protected T BaseStream { get; set; } + /// <summary> + /// A value that will cause all calls to write to throw <see cref="NotSupportedException"/> + /// </summary> + protected bool ForceReadOnly { get; set; } + ///<inheritdoc/> + public override bool CanRead => BaseStream.CanRead; + ///<inheritdoc/> + public override bool CanSeek => BaseStream.CanSeek; + ///<inheritdoc/> + public override bool CanWrite => BaseStream.CanWrite && !ForceReadOnly; + ///<inheritdoc/> + public override long Length => BaseStream.Length; + ///<inheritdoc/> + public override int WriteTimeout { get => BaseStream.WriteTimeout; set => BaseStream.WriteTimeout = value; } + ///<inheritdoc/> + public override int ReadTimeout { get => BaseStream.ReadTimeout; set => BaseStream.ReadTimeout = value; } + ///<inheritdoc/> + public override long Position { get => BaseStream.Position; set => BaseStream.Position = value; } + ///<inheritdoc/> + public override void Flush() + { + BaseStream.Flush(); + OnFlush(); + } + ///<inheritdoc/> + public override int Read(byte[] buffer, int offset, int count) => BaseStream.Read(buffer, offset, count); + ///<inheritdoc/> + public override int Read(Span<byte> buffer) => BaseStream.Read(buffer); + ///<inheritdoc/> + public override long Seek(long offset, SeekOrigin origin) => BaseStream.Seek(offset, origin); + ///<inheritdoc/> + public override void SetLength(long value) => BaseStream.SetLength(value); + ///<inheritdoc/> + public override void Write(byte[] buffer, int offset, int count) + { + if (ForceReadOnly) + { + throw new NotSupportedException("Stream is set to readonly mode"); + } + BaseStream.Write(buffer, offset, count); + //Call onwrite function + OnWrite(count); + } + ///<inheritdoc/> + public override void Write(ReadOnlySpan<byte> buffer) + { + if (ForceReadOnly) + { + throw new NotSupportedException("Stream is set to readonly mode"); + } + BaseStream.Write(buffer); + //Call onwrite function + OnWrite(buffer.Length); + } + ///<inheritdoc/> + public override void Close() + { + BaseStream.Close(); + //Call on close function + OnClose(); + } + + /// <summary> + /// Raised directly after the base stream is closed, when a call to close is made + /// </summary> + protected virtual void OnClose() { } + /// <summary> + /// Raised directly after the base stream is flushed, when a call to flush is made + /// </summary> + protected virtual void OnFlush() { } + /// <summary> + /// Raised directly after a successfull write operation. + /// </summary> + /// <param name="count">The number of bytes written to the stream</param> + protected virtual void OnWrite(int count) { } + + ///<inheritdoc/> + public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return BaseStream.ReadAsync(buffer, offset, count, cancellationToken); + } + ///<inheritdoc/> + public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) + { + return BaseStream.ReadAsync(buffer, cancellationToken); + } + ///<inheritdoc/> + public override void CopyTo(Stream destination, int bufferSize) => BaseStream.CopyTo(destination, bufferSize); + ///<inheritdoc/> + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return BaseStream.CopyToAsync(destination, bufferSize, cancellationToken); + } + ///<inheritdoc/> + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (ForceReadOnly) + { + throw new NotSupportedException("Stream is set to readonly mode"); + } + //We want to maintain pass through as much as possible, so supress warning +#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' + await BaseStream.WriteAsync(buffer, offset, count, cancellationToken); +#pragma warning restore CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' + + //Call on-write and pass the number of bytes written + OnWrite(count); + } + ///<inheritdoc/> + public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) + { + if (ForceReadOnly) + { + throw new NotSupportedException("Stream is set to readonly mode"); + } + await BaseStream.WriteAsync(buffer, cancellationToken); + //Call on-write and pass the length + OnWrite(buffer.Length); + } + ///<inheritdoc/> + public override async Task FlushAsync(CancellationToken cancellationToken) + { + await BaseStream.FlushAsync(cancellationToken); + //Call onflush + OnFlush(); + } + + ///<inheritdoc/> + public override async ValueTask DisposeAsync() + { + //Dispose the base stream and await it + await BaseStream.DisposeAsync(); + //Call onclose + OnClose(); + //Suppress finalize + GC.SuppressFinalize(this); + } + } +}
\ No newline at end of file diff --git a/Utils/src/IO/FileOperations.cs b/Utils/src/IO/FileOperations.cs new file mode 100644 index 0000000..e040da4 --- /dev/null +++ b/Utils/src/IO/FileOperations.cs @@ -0,0 +1,105 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: FileOperations.cs +* +* FileOperations.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; +using System.IO; +using System.Runtime.InteropServices; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Contains cross-platform optimized filesystem operations. + /// </summary> + public static class FileOperations + { + public const int INVALID_FILE_ATTRIBUTES = -1; + + [DllImport("Shlwapi", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return:MarshalAs(UnmanagedType.Bool)] + private static unsafe extern bool PathFileExists(char* path); + [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return:MarshalAs(UnmanagedType.I4)] + private static unsafe extern int GetFileAttributes(char* path); + + static readonly bool IsWindows = OperatingSystem.IsWindows(); + /// <summary> + /// Determines if a file exists. If application is current running in the Windows operating system, Shlwapi.PathFileExists is invoked, + /// otherwise <see cref="File.Exists(string?)"/> is invoked + /// </summary> + /// <param name="filePath">the path to the file</param> + /// <returns>True if the file can be opened, false otherwise</returns> + public static bool FileExists(string filePath) + { + //If windows is detected, use the unmanged function + if (!IsWindows) + { + return File.Exists(filePath); + } + unsafe + { + //Get a char pointer to the file path + fixed (char* path = filePath) + { + //Invoke the winap file function + return PathFileExists(path); + } + } + } + + /// <summary> + /// If Windows is detected at load time, gets the attributes for the specified file. + /// </summary> + /// <param name="filePath">The path to the existing file</param> + /// <returns>The attributes of the file </returns> + /// <exception cref="PathTooLongException"></exception> + /// <exception cref="FileNotFoundException"></exception> + /// <exception cref="UnauthorizedAccessException"></exception> + public static FileAttributes GetAttributes(string filePath) + { + //If windows is detected, use the unmanged function + if (!IsWindows) + { + return File.GetAttributes(filePath); + } + unsafe + { + //Get a char pointer to the file path + fixed (char* path = filePath) + { + //Invoke the winap file function and cast the returned int value to file attributes + int attr = GetFileAttributes(path); + //Check for error + if (attr == INVALID_FILE_ATTRIBUTES) + { + throw new FileNotFoundException("The requested file was not found", filePath); + } + //Cast to file attributes and return + return (FileAttributes)attr; + } + } + } + } +}
\ No newline at end of file diff --git a/Utils/src/IO/IDataAccumulator.cs b/Utils/src/IO/IDataAccumulator.cs new file mode 100644 index 0000000..5129a55 --- /dev/null +++ b/Utils/src/IO/IDataAccumulator.cs @@ -0,0 +1,64 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IDataAccumulator.cs +* +* IDataAccumulator.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.IO +{ + /// <summary> + /// A data structure that represents a sliding window over a buffer + /// for resetable forward-only reading or writing + /// </summary> + /// <typeparam name="T">The accumuation data type</typeparam> + public interface IDataAccumulator<T> + { + /// <summary> + /// Gets the number of available items within the buffer + /// </summary> + int AccumulatedSize { get; } + /// <summary> + /// The number of elements remaining in the buffer + /// </summary> + int RemainingSize { get; } + /// <summary> + /// The remaining space in the internal buffer as a contiguous segment + /// </summary> + Span<T> Remaining { get; } + /// <summary> + /// The buffer window over the accumulated data + /// </summary> + Span<T> Accumulated { get; } + + /// <summary> + /// Advances the accumulator buffer window by the specified amount + /// </summary> + /// <param name="count">The number of elements accumulated</param> + void Advance(int count); + + /// <summary> + /// Resets the internal state of the accumulator + /// </summary> + void Reset(); + } +}
\ No newline at end of file diff --git a/Utils/src/IO/ISlindingWindowBuffer.cs b/Utils/src/IO/ISlindingWindowBuffer.cs new file mode 100644 index 0000000..ff4e142 --- /dev/null +++ b/Utils/src/IO/ISlindingWindowBuffer.cs @@ -0,0 +1,91 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ISlindingWindowBuffer.cs +* +* ISlindingWindowBuffer.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.IO +{ + /// <summary> + /// Represents a sliding window buffer for reading/wiriting data + /// </summary> + /// <typeparam name="T"></typeparam> + public interface ISlindingWindowBuffer<T> : IDataAccumulator<T> + { + /// <summary> + /// The number of elements remaining in the buffer + /// </summary> + int IDataAccumulator<T>.RemainingSize => Buffer.Length - WindowEndPos; + /// <summary> + /// The remaining space in the internal buffer as a contiguous segment + /// </summary> + Span<T> IDataAccumulator<T>.Remaining => RemainingBuffer.Span; + /// <summary> + /// The buffer window over the accumulated data + /// </summary> + Span<T> IDataAccumulator<T>.Accumulated => AccumulatedBuffer.Span; + /// <summary> + /// Gets the number of available items within the buffer + /// </summary> + int IDataAccumulator<T>.AccumulatedSize => WindowEndPos - WindowStartPos; + + /// <summary> + /// The starting positon of the available data within the buffer + /// </summary> + int WindowStartPos { get; } + /// <summary> + /// The ending position of the available data within the buffer + /// </summary> + int WindowEndPos { get; } + /// <summary> + /// Buffer memory wrapper + /// </summary> + Memory<T> Buffer { get; } + + /// <summary> + /// Releases resources used by the current instance + /// </summary> + void Close(); + /// <summary> + /// <para> + /// Advances the begining of the accumulated data window. + /// </para> + /// <para> + /// This method is used during reading to singal that data + /// has been read from the internal buffer and the + /// accumulator window can be shifted. + /// </para> + /// </summary> + /// <param name="count">The number of elements to shift by</param> + void AdvanceStart(int count); + + /// <summary> + /// Gets a window within the buffer of available buffered data + /// </summary> + Memory<T> AccumulatedBuffer => Buffer[WindowStartPos..WindowEndPos]; + /// <summary> + /// Gets the available buffer window to write data to + /// </summary> + Memory<T> RemainingBuffer => Buffer[WindowEndPos..]; + } +}
\ No newline at end of file diff --git a/Utils/src/IO/IVnTextReader.cs b/Utils/src/IO/IVnTextReader.cs new file mode 100644 index 0000000..625ba78 --- /dev/null +++ b/Utils/src/IO/IVnTextReader.cs @@ -0,0 +1,72 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IVnTextReader.cs +* +* IVnTextReader.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; +using System.IO; +using System.Text; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Represents a streaming text reader with internal buffers + /// </summary> + public interface IVnTextReader + { + /// <summary> + /// The base stream to read data from + /// </summary> + Stream BaseStream { get; } + /// <summary> + /// The character encoding used by the TextReader + /// </summary> + Encoding Encoding { get; } + /// <summary> + /// Number of available bytes of buffered data within the current buffer window + /// </summary> + int Available { get; } + /// <summary> + /// Gets or sets the line termination used to deliminate a line of data + /// </summary> + ReadOnlyMemory<byte> LineTermination { get; } + /// <summary> + /// The unread/available data within the internal buffer + /// </summary> + Span<byte> BufferedDataWindow { get; } + /// <summary> + /// Shifts the sliding buffer window by the specified number of bytes. + /// </summary> + /// <param name="count">The number of bytes read from the buffer</param> + void Advance(int count); + /// <summary> + /// Reads data from the stream into the remaining buffer space for processing + /// </summary> + void FillBuffer(); + /// <summary> + /// Compacts the available buffer space back to the begining of the buffer region + /// and determines if there is room for more data to be buffered + /// </summary> + /// <returns>The remaining buffer space if any</returns> + ERRNO CompactBufferWindow(); + } +}
\ No newline at end of file diff --git a/Utils/src/IO/InMemoryTemplate.cs b/Utils/src/IO/InMemoryTemplate.cs new file mode 100644 index 0000000..12f9092 --- /dev/null +++ b/Utils/src/IO/InMemoryTemplate.cs @@ -0,0 +1,196 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: InMemoryTemplate.cs +* +* InMemoryTemplate.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; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Represents a lazily loaded file stored in memory, with a change mointor + /// that reloads the template if the file was modified in the filesystem + /// </summary> + public abstract class InMemoryTemplate : VnDisposeable + { + protected ManualResetEventSlim TemplateLock; + private readonly FileSystemWatcher? Watcher; + private bool Modified; + private VnMemoryStream templateBuffer; + protected readonly FileInfo TemplateFile; + + /// <summary> + /// Gets the name of the template + /// </summary> + public abstract string TemplateName { get; } + + /// <summary> + /// Creates a new in-memory copy of a file that will detect changes and refresh + /// </summary> + /// <param name="listenForChanges">Should changes to the template file be moniored for changes, and reloaded as necessary</param> + /// <param name="path">The path of the file template</param> + protected InMemoryTemplate(string path, bool listenForChanges = true) + { + TemplateFile = new FileInfo(path); + TemplateLock = new(true); + //Make sure the file exists + if (!TemplateFile.Exists) + { + throw new FileNotFoundException("Template file does not exist"); + } + if (listenForChanges) + { + //Setup a watcher to reload the template when modified + Watcher = new FileSystemWatcher(TemplateFile.DirectoryName) + { + EnableRaisingEvents = true, + IncludeSubdirectories = false, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size + }; + Watcher.Changed += Watcher_Changed; + } + //Set modified flag to make sure the template is read on first use + this.Modified = true; + } + + private void Watcher_Changed(object sender, FileSystemEventArgs e) + { + //Make sure the event was raied for this template + if (!e.FullPath.Equals(TemplateFile.FullName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + TemplateLock.Reset(); + try + { + //Set modified flag + Modified = true; + //Refresh the fileinfo object + TemplateFile.Refresh(); + //Invoke onmodifed function + OnModifed(); + } + finally + { + TemplateLock.Set(); + } + } + + /// <summary> + /// Gets a cached copy of the template data + /// </summary> + protected VnMemoryStream GetTemplateData() + { + //Make sure access is synchronized incase the file gets updated during access on another thread + TemplateLock.Wait(); + //Determine if the file has been modified and needs to be reloaded + if (Modified) + { + TemplateLock.Reset(); + try + { + //Read a new copy of the templte into mem + ReadFile(); + } + finally + { + TemplateLock.Set(); + } + } + //Return a copy of the memory stream + return templateBuffer.GetReadonlyShallowCopy(); + } + /// <summary> + /// Updates the internal copy of the file to its memory representation + /// </summary> + protected void ReadFile() + { + //Open the file stream + using FileStream fs = TemplateFile.OpenRead(); + //Dispose the old template buffer + templateBuffer?.Dispose(); + //Create a new stream for storing the cached copy + VnMemoryStream newBuf = new(); + try + { + fs.CopyTo(newBuf, null); + } + catch + { + newBuf.Dispose(); + throw; + } + //Create the readonly copy + templateBuffer = VnMemoryStream.CreateReadonly(newBuf); + //Clear the modified flag + Modified = false; + } + /// <summary> + /// Updates the internal copy of the file to its memory representation, asynchronously + /// </summary> + /// <param name="cancellationToken"></param> + /// <returns>A task that completes when the file has been copied into memory</returns> + protected async Task ReadFileAsync(CancellationToken cancellationToken = default) + { + //Open the file stream + await using FileStream fs = TemplateFile.OpenRead(); + //Dispose the old template buffer + templateBuffer?.Dispose(); + //Create a new stream for storing the cached copy + VnMemoryStream newBuf = new(); + try + { + //Copy async + await fs.CopyToAsync(newBuf, 8192, Memory.Memory.Shared, cancellationToken); + } + catch + { + newBuf.Dispose(); + throw; + } + //Create the readonly copy + templateBuffer = VnMemoryStream.CreateReadonly(newBuf); + //Clear the modified flag + Modified = false; + } + + /// <summary> + /// Invoked when the template file has been modifed. Note: This event is raised + /// while the <see cref="TemplateLock"/> is held. + /// </summary> + protected abstract void OnModifed(); + + ///<inheritdoc/> + protected override void Free() + { + //Dispose the watcher + Watcher?.Dispose(); + //free the stream + templateBuffer?.Dispose(); + } + } +}
\ No newline at end of file diff --git a/Utils/src/IO/IsolatedStorageDirectory.cs b/Utils/src/IO/IsolatedStorageDirectory.cs new file mode 100644 index 0000000..65460ff --- /dev/null +++ b/Utils/src/IO/IsolatedStorageDirectory.cs @@ -0,0 +1,154 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IsolatedStorageDirectory.cs +* +* IsolatedStorageDirectory.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; +using System.IO; +using System.IO.IsolatedStorage; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Represents an open directory within an <see cref="IsolatedStorageFile"/> store for which files can be created, opened, or deleted. + /// </summary> + public sealed class IsolatedStorageDirectory : IsolatedStorage + { + private readonly string DirectoryPath; + private readonly IsolatedStorageFile Storage; + /// <summary> + /// Creates a new <see cref="IsolatedStorageDirectory"/> within the specified file using the directory name. + /// </summary> + /// <param name="storage">A configured and open <see cref="IsolatedStorageFile"/></param> + /// <param name="dir">The directory name to open or create within the store</param> + public IsolatedStorageDirectory(IsolatedStorageFile storage, string dir) + { + this.Storage = storage; + this.DirectoryPath = dir; + //If the directory doesnt exist, create it + if (!this.Storage.DirectoryExists(dir)) + this.Storage.CreateDirectory(dir); + } + + private IsolatedStorageDirectory(IsolatedStorageDirectory parent, string dirName) + { + //Store ref to parent dir + Parent = parent; + //Referrence store + this.Storage = parent.Storage; + //Add the name of this dir to the end of the specified dir path + this.DirectoryPath = Path.Combine(parent.DirectoryPath, dirName); + } + + /// <summary> + /// Creates a file by its path name within the currnet directory + /// </summary> + /// <param name="fileName">The name of the file</param> + /// <returns>The open file</returns> + /// <exception cref="IsolatedStorageException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="DirectoryNotFoundException"></exception> + public IsolatedStorageFileStream CreateFile(string fileName) + { + return this.Storage.CreateFile(Path.Combine(DirectoryPath, fileName)); + } + /// <summary> + /// Removes a file from the current directory + /// </summary> + /// <param name="fileName">The path of the file to remove</param> + /// <exception cref="IsolatedStorageException"></exception> + public void DeleteFile(string fileName) + { + this.Storage.DeleteFile(Path.Combine(this.DirectoryPath, fileName)); + } + /// <summary> + /// Opens a file that exists within the current directory + /// </summary> + /// <param name="fileName">Name with extension of the file</param> + /// <param name="mode">File mode</param> + /// <param name="access">File access</param> + /// <returns>The open <see cref="IsolatedStorageFileStream"/> from the current directory</returns> + public IsolatedStorageFileStream OpenFile(string fileName, FileMode mode, FileAccess access) + { + return this.Storage.OpenFile(Path.Combine(DirectoryPath, fileName), mode, access); + } + /// <summary> + /// Opens a file that exists within the current directory + /// </summary> + /// <param name="fileName">Name with extension of the file</param> + /// <param name="mode">File mode</param> + /// <param name="access">File access</param> + /// <param name="share">The file shareing mode</param> + /// <returns>The open <see cref="IsolatedStorageFileStream"/> from the current directory</returns> + public IsolatedStorageFileStream OpenFile(string fileName, FileMode mode, FileAccess access, FileShare share) + { + return this.Storage.OpenFile(Path.Combine(DirectoryPath, fileName), mode, access, share); + } + + /// <summary> + /// Determiens if the specified file path refers to an existing file within the directory + /// </summary> + /// <param name="fileName">The name of the file to search for</param> + /// <returns>True if the file exists within the current directory</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="IsolatedStorageException"></exception> + /// <exception cref="InvalidOperationException"></exception> + public bool FileExists(string fileName) + { + return this.Storage.FileExists(Path.Combine(this.DirectoryPath, fileName)); + } + + /// <summary> + /// Removes the directory and its contents from the store + /// </summary> + public override void Remove() + { + Storage.DeleteDirectory(this.DirectoryPath); + } + + public override long AvailableFreeSpace => Storage.AvailableFreeSpace; + public override long Quota => Storage.Quota; + public override long UsedSize => Storage.UsedSize; + public override bool IncreaseQuotaTo(long newQuotaSize) => Storage.IncreaseQuotaTo(newQuotaSize); + + /// <summary> + /// The parent <see cref="IsolatedStorageDirectory"/> this directory is a child within. null if there are no parent directories + /// above this dir + /// </summary> + + public IsolatedStorageDirectory? Parent { get; } +#nullable disable + + /// <summary> + /// Creates a child directory within the current directory + /// </summary> + /// <param name="directoryName">The name of the child directory</param> + /// <returns>A new <see cref="IsolatedStorageDirectory"/> for which <see cref="IsolatedStorageFileStream"/>s can be opened/created</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> + public IsolatedStorageDirectory CreateChildDirectory(string directoryName) + { + return new IsolatedStorageDirectory(this, directoryName); + } + } +}
\ No newline at end of file diff --git a/Utils/src/IO/SlidingWindowBufferExtensions.cs b/Utils/src/IO/SlidingWindowBufferExtensions.cs new file mode 100644 index 0000000..0509061 --- /dev/null +++ b/Utils/src/IO/SlidingWindowBufferExtensions.cs @@ -0,0 +1,213 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: SlidingWindowBufferExtensions.cs +* +* SlidingWindowBufferExtensions.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; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; +using System.Runtime.CompilerServices; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Extention methods for <see cref="ISlindingWindowBuffer{T}"/> + /// </summary> + public static class SlidingWindowBufferExtensions + { + /// <summary> + /// Shifts/resets the current buffered data window down to the + /// begining of the buffer if the buffer window is shifted away + /// from the begining. + /// </summary> + /// <returns>The number of bytes of available space in the buffer</returns> + public static ERRNO CompactBufferWindow<T>(this ISlindingWindowBuffer<T> sBuf) + { + //Nothing to compact if the starting data pointer is at the beining of the window + if (sBuf.WindowStartPos > 0) + { + //Get span over engire buffer + Span<T> buffer = sBuf.Buffer.Span; + //Get data within window + Span<T> usedData = sBuf.Accumulated; + //Copy remaining to the begining of the buffer + usedData.CopyTo(buffer); + + //Reset positions, then advance to the specified size + sBuf.Reset(); + sBuf.Advance(usedData.Length); + } + //Return the number of bytes of available space + return sBuf.RemainingSize; + } + + /// <summary> + /// Appends the specified data to the end of the buffer + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="sBuf"></param> + /// <param name="val">The value to append to the end of the buffer</param> + /// <exception cref="IndexOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append<T>(this IDataAccumulator<T> sBuf, T val) + { + //Set the value at first position + sBuf.Remaining[0] = val; + //Advance by 1 + sBuf.Advance(1); + } + /// <summary> + /// Appends the specified data to the end of the buffer + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="sBuf"></param> + /// <param name="val">The value to append to the end of the buffer</param> + /// <exception cref="ArgumentException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append<T>(this IDataAccumulator<T> sBuf, ReadOnlySpan<T> val) + { + val.CopyTo(sBuf.Remaining); + sBuf.Advance(val.Length); + } + /// <summary> + /// Formats and appends a value type to the accumulator with proper endianess + /// </summary> + /// <typeparam name="T">The value type to appent</typeparam> + /// <param name="accumulator">The binary accumulator to append</param> + /// <param name="value">The value type to append</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append<T>(this IDataAccumulator<byte> accumulator, T value) where T: unmanaged + { + //Use forward reader for the memory extension to append a value type to a binary accumulator + ForwardOnlyWriter<byte> w = new(accumulator.Remaining); + w.Append(value); + accumulator.Advance(w.Written); + } + + /// <summary> + /// Attempts to write as much data as possible to the remaining space + /// in the buffer and returns the number of bytes accumulated. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="accumulator"></param> + /// <param name="value">The value to accumulate</param> + /// <returns>The number of bytes accumulated</returns> + public static ERRNO TryAccumulate<T>(this IDataAccumulator<T> accumulator, ReadOnlySpan<T> value) + { + //Calc data size and reserve space for final crlf + int dataToCopy = Math.Min(value.Length, accumulator.RemainingSize); + + //Write as much data as possible + accumulator.Append(value[..dataToCopy]); + + //Return number of bytes not written + return dataToCopy; + } + + /// <summary> + /// Appends a <see cref="ISpanFormattable"/> instance to the end of the accumulator + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="accumulator"></param> + /// <param name="formattable">The formattable instance to write to the accumulator</param> + /// <param name="format">The format arguments</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append<T>(this IDataAccumulator<char> accumulator, in T formattable, ReadOnlySpan<char> format = default) where T : struct, ISpanFormattable + { + ForwardOnlyWriter<char> writer = new(accumulator.Remaining); + writer.Append(formattable, format); + accumulator.Advance(writer.Written); + } + + /// <summary> + /// Uses the remaining data buffer to compile a <see cref="IStringSerializeable"/> + /// instance, then advances the accumulator by the number of characters used. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="accumulator"></param> + /// <param name="compileable">The <see cref="IStringSerializeable"/> instance to compile</param> + public static void Append<T>(this IDataAccumulator<char> accumulator, in T compileable) where T : IStringSerializeable + { + //Write directly to the remaining space + int written = compileable.Compile(accumulator.Remaining); + //Advance the writer + accumulator.Advance(written); + } + + /// <summary> + /// Reads available data from the current window and writes as much as possible it to the supplied buffer + /// and advances the buffer window + /// </summary> + /// <typeparam name="T">Element type</typeparam> + /// <param name="sBuf"></param> + /// <param name="buffer">The output buffer to write data to</param> + /// <returns>The number of elements written to the buffer</returns> + public static ERRNO Read<T>(this ISlindingWindowBuffer<T> sBuf, in Span<T> buffer) + { + //Calculate the amount of data to copy + int dataToCopy = Math.Min(buffer.Length, sBuf.AccumulatedSize); + //Copy the data to the buffer + sBuf.Accumulated[..dataToCopy].CopyTo(buffer); + //Advance the window + sBuf.AdvanceStart(dataToCopy); + //Return the number of bytes copied + return dataToCopy; + } + + /// <summary> + /// Fills the remaining window space of the current accumulator with + /// data from the specified stream asynchronously. + /// </summary> + /// <param name="accumulator"></param> + /// <param name="input">The stream to read data from</param> + /// <param name="cancellationToken">A token to cancel the operation</param> + /// <returns>A value task representing the operation</returns> + public static async ValueTask AccumulateDataAsync(this ISlindingWindowBuffer<byte> accumulator, Stream input, CancellationToken cancellationToken) + { + //Get a buffer from the end of the current window to the end of the buffer + Memory<byte> bufWindow = accumulator.RemainingBuffer; + //Read from stream async + int read = await input.ReadAsync(bufWindow, cancellationToken); + //Update the end of the buffer window to the end of the read data + accumulator.Advance(read); + } + /// <summary> + /// Fills the remaining window space of the current accumulator with + /// data from the specified stream. + /// </summary> + /// <param name="accumulator"></param> + /// <param name="input">The stream to read data from</param> + public static void AccumulateData(this IDataAccumulator<byte> accumulator, Stream input) + { + //Get a buffer from the end of the current window to the end of the buffer + Span<byte> bufWindow = accumulator.Remaining; + //Read from stream async + int read = input.Read(bufWindow); + //Update the end of the buffer window to the end of the read data + accumulator.Advance(read); + } + } +}
\ No newline at end of file diff --git a/Utils/src/IO/TemporayIsolatedFile.cs b/Utils/src/IO/TemporayIsolatedFile.cs new file mode 100644 index 0000000..3bee92b --- /dev/null +++ b/Utils/src/IO/TemporayIsolatedFile.cs @@ -0,0 +1,57 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: TemporayIsolatedFile.cs +* +* TemporayIsolatedFile.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; +using System.IO; +using System.IO.IsolatedStorage; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Allows for temporary files to be generated, used, then removed from an <see cref="IsolatedStorageFile"/> + /// </summary> + public sealed class TemporayIsolatedFile : BackingStream<IsolatedStorageFileStream> + { + private readonly IsolatedStorageDirectory Storage; + private readonly string Filename; + /// <summary> + /// Creates a new temporary filestream within the specified <see cref="IsolatedStorageFile"/> + /// </summary> + /// <param name="storage">The file store to genreate temporary files within</param> + public TemporayIsolatedFile(IsolatedStorageDirectory storage) + { + //Store ref + this.Storage = storage; + //Creaet a new random filename + this.Filename = Path.GetRandomFileName(); + //try to created a new file within the isolaged storage + this.BaseStream = storage.CreateFile(this.Filename); + } + protected override void OnClose() + { + //Remove the file from the storage + Storage.DeleteFile(this.Filename); + } + } +}
\ No newline at end of file diff --git a/Utils/src/IO/VnMemoryStream.cs b/Utils/src/IO/VnMemoryStream.cs new file mode 100644 index 0000000..389a7da --- /dev/null +++ b/Utils/src/IO/VnMemoryStream.cs @@ -0,0 +1,469 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnMemoryStream.cs +* +* VnMemoryStream.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; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.InteropServices; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.IO +{ + + using Utils.Memory; + + /// <summary> + /// Provides an unmanaged memory stream. Desigend to help reduce garbage collector load for + /// high frequency memory operations. Similar to <see cref="UnmanagedMemoryStream"/> + /// </summary> + public sealed class VnMemoryStream : Stream, ICloneable + { + private long _position; + private long _length; + //Memory + private readonly MemoryHandle<byte> _buffer; + private bool IsReadonly; + //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="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, Int64 length, bool readOnly) + { + handle.ThrowIfClosed(); + return new VnMemoryStream(handle, length, readOnly, true); + } + + /// <summary> + /// Converts a writable <see cref="VnMemoryStream"/> to readonly to allow shallow copies + /// </summary> + /// <param name="stream">The stream to make readonly</param> + /// <returns>The readonly stream</returns> + public static VnMemoryStream CreateReadonly(VnMemoryStream stream) + { + //Set the readonly flag + stream.IsReadonly = true; + //Return the stream + return stream; + } + + /// <summary> + /// Creates a new memory stream + /// </summary> + public VnMemoryStream() : this(Memory.Shared) { } + /// <summary> + /// Create a new memory stream where buffers will be allocated from the specified heap + /// </summary> + /// <param name="heap"><see cref="PrivateHeap"/> to allocate memory from</param> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ArgumentNullException"></exception> + public VnMemoryStream(IUnmangedHeap heap) : this(heap, 0, false) { } + + /// <summary> + /// Creates a new memory stream and pre-allocates the internal + /// buffer of the specified size on the specified heap to avoid resizing. + /// </summary> + /// <param name="heap"><see cref="PrivateHeap"/> to allocate memory from</param> + /// <param name="bufferSize">Number of bytes (length) of the stream if known</param> + /// <param name="zero">Zero memory allocations during buffer expansions</param> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public VnMemoryStream(IUnmangedHeap heap, long bufferSize, bool zero) + { + _ = heap ?? throw new ArgumentNullException(nameof(heap)); + _buffer = heap.Alloc<byte>(bufferSize, zero); + } + + /// <summary> + /// Creates a new memory stream from the data provided + /// </summary> + /// <param name="heap"><see cref="PrivateHeap"/> to allocate memory from</param> + /// <param name="data">Initial data</param> + public VnMemoryStream(IUnmangedHeap heap, ReadOnlySpan<byte> data) + { + _ = heap ?? throw new ArgumentNullException(nameof(heap)); + //Alloc the internal buffer to match the data stream + _buffer = heap.AllocAndCopy(data); + //Set length + _length = data.Length; + //Position will default to 0 cuz its dotnet :P + return; + } + + /// <summary> + /// WARNING: Dangerous constructor, make sure read-only and owns hanlde are set accordingly + /// </summary> + /// <param name="buffer">The buffer to referrence directly</param> + /// <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, long length, bool readOnly, bool ownsHandle) + { + OwnsHandle = ownsHandle; + _buffer = buffer; //Consume the handle + _length = length; //Store length of the buffer + IsReadonly = readOnly; + } + + /// <summary> + /// UNSAFE Number of bytes between position and length. Never negative + /// </summary> + private long LenToPosDiff => Math.Max(_length - _position, 0); + + /// <summary> + /// If the current stream is a readonly stream, creates an unsafe shallow copy for reading only. + /// </summary> + /// <returns>New stream shallow copy of the internal stream</returns> + /// <exception cref="NotSupportedException"></exception> + public VnMemoryStream GetReadonlyShallowCopy() + { + //Create a new readonly copy (stream does not own the handle) + return !IsReadonly + ? throw new NotSupportedException("This stream is not readonly. Cannot create shallow copy on a mutable stream") + : new VnMemoryStream(_buffer, _length, true, false); + } + + /// <summary> + /// Writes data directly to the destination stream from the internal buffer + /// without allocating or copying any data. + /// </summary> + /// <param name="destination">The stream to write data to</param> + /// <param name="bufferSize">The size of the chunks to write to the destination stream</param> + /// <exception cref="IOException"></exception> + public override void CopyTo(Stream destination, int bufferSize) + { + _ = destination ?? throw new ArgumentNullException(nameof(destination)); + + if (!destination.CanWrite) + { + throw new IOException("The destinaion stream is not writeable"); + } + + do + { + //Calc the remaining bytes to read no larger than the buffer size + int bytesToRead = (int)Math.Min(LenToPosDiff, bufferSize); + + //Create a span wrapper by using the offet function to support memory handles larger than 2gb + ReadOnlySpan<byte> span = _buffer.GetOffsetSpan(_position, bytesToRead); + + destination.Write(span); + + //Update position + _position += bytesToRead; + + } while (LenToPosDiff > 0); + } + + /// <summary> + /// Allocates a temporary buffer of the desired size, copies data from the internal + /// buffer and writes it to the destination buffer asynchronously. + /// </summary> + /// <param name="destination">The stream to write output data to</param> + /// <param name="bufferSize">The size of the buffer to use when copying data</param> + /// <param name="cancellationToken">A token to cancel the opreation</param> + /// <returns>A task that resolves when the remaining data in the stream has been written to the destination</returns> + /// <exception cref="IOException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + _ = destination ?? throw new ArgumentNullException(nameof(destination)); + + 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 + */ + + using VnTempBuffer<byte> copyBuffer = new(bufferSize); + + do + { + //read from internal stream + int read = Read(copyBuffer); + + if(read <= 0) + { + break; + } + + //write async + await destination.WriteAsync(copyBuffer.AsMemory(0, read), cancellationToken); + + } while (true); + + } + + /// <summary> + /// <inheritdoc/> + /// <para> + /// This property is always true + /// </para> + /// </summary> + public override bool CanRead => true; + /// <summary> + /// <inheritdoc/> + /// <para> + /// This propery is always true + /// </para> + /// </summary> + public override bool CanSeek => true; + /// <summary> + /// True unless the stream is (or has been converted to) a readonly + /// stream. + /// </summary> + public override bool CanWrite => !IsReadonly; + ///<inheritdoc/> + public override long Length => _length; + ///<inheritdoc/> + public override bool CanTimeout => false; + + ///<inheritdoc/> + public override long Position + { + get => _position; + set => Seek(value, SeekOrigin.Begin); + } + /// <summary> + /// Closes the stream and frees the internal allocated memory blocks + /// </summary> + public override void Close() + { + //Only dispose buffer if we own it + if (OwnsHandle) + { + _buffer.Dispose(); + } + } + ///<inheritdoc/> + public override void Flush() { } + // Override to reduce base class overhead + ///<inheritdoc/> + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + ///<inheritdoc/> + public override int Read(byte[] buffer, int offset, int count) => Read(new Span<byte>(buffer, offset, count)); + ///<inheritdoc/> + public override int Read(Span<byte> destination) + { + if (destination.Length == 0) + { + return 0; + } + //Number of bytes to read from memory buffer + int bytesToRead = checked((int)Math.Min(LenToPosDiff, destination.Length)); + //Copy bytes to buffer + Memory.Copy(_buffer, _position, destination, 0, bytesToRead); + //Increment buffer position + _position += bytesToRead; + //Bytestoread should never be larger than int.max because span length is an integer + return bytesToRead; + } + + /* + * Async reading will always run synchronously in a memory stream, + * so overrides are just so avoid base class overhead + */ + ///<inheritdoc/> + public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) + { + //Read synchronously and return a completed task + int read = Read(buffer.Span); + return ValueTask.FromResult(read); + } + ///<inheritdoc/> + public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + //Read synchronously and return a completed task + int read = Read(buffer.AsSpan(offset, count)); + return Task.FromResult(read); + } + ///<inheritdoc/> + public override long Seek(long offset, SeekOrigin origin) + { + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Offset cannot be less than 0"); + } + switch (origin) + { + case SeekOrigin.Begin: + //Length will never be greater than int.Max so output will never exceed int.max + _position = Math.Min(_length, offset); + return _position; + case SeekOrigin.Current: + long newPos = _position + offset; + //Length will never be greater than int.Max so output will never exceed length + _position = Math.Min(_length, newPos); + return newPos; + case SeekOrigin.End: + long real_index = _length - offset; + //If offset moves the position negative, just set the position to 0 and continue + _position = Math.Min(real_index, 0); + return real_index; + default: + throw new ArgumentException("Stream operation is not supported on current stream"); + } + } + + + /// <summary> + /// Resizes the internal buffer to the exact size (in bytes) of the + /// value argument. A value of 0 will free the entire buffer. A value + /// greater than zero will resize the buffer (and/or alloc) + /// </summary> + /// <param name="value">The size of the stream (and internal buffer)</param> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public override void SetLength(long value) + { + if (IsReadonly) + { + throw new NotSupportedException("This stream is readonly"); + } + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "Value cannot be less than 0"); + } + //Resize the buffer to the specified length + _buffer.Resize(value); + //Set length + _length = value; + //Make sure the position is not pointing outside of the buffer + _position = Math.Min(_position, _length); + return; + } + ///<inheritdoc/> + public override void Write(byte[] buffer, int offset, int count) => Write(new ReadOnlySpan<byte>(buffer, offset, count)); + ///<inheritdoc/> + public override void Write(ReadOnlySpan<byte> buffer) + { + if (IsReadonly) + { + throw new NotSupportedException("Write operation is not allowed on readonly stream!"); + } + //Calculate the new final position + long newPos = (_position + buffer.Length); + //Determine if the buffer needs to be expanded + if (buffer.Length > LenToPosDiff) + { + //Expand buffer if required + _buffer.ResizeIfSmaller(newPos); + //Update length + _length = newPos; + } + //Copy the input buffer to the internal buffer + Memory.Copy(buffer, _buffer, _position); + //Update the position + _position = newPos; + return; + } + ///<inheritdoc/> + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + //Write synchronously and return a completed task + Write(buffer, offset, count); + return Task.CompletedTask; + } + ///<inheritdoc/> + public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) + { + //Write synchronously and return a completed task + Write(buffer.Span); + return ValueTask.CompletedTask; + } + ///<inheritdoc/> + public override void WriteByte(byte value) + { + Span<byte> buf = MemoryMarshal.CreateSpan(ref value, 1); + Write(buf); + } + + /// <summary> + /// Allocates and copies internal buffer to new managed byte[] + /// </summary> + /// <returns>Copy of internal buffer</returns> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + public byte[] ToArray() + { + //Alloc a new array of the size of the internal buffer + byte[] data = new byte[_length]; + //Copy data from the internal buffer to the output buffer + _buffer.Span.CopyTo(data); + return data; + + } + /// <summary> + /// Returns a <see cref="ReadOnlySpan{T}"/> window over the data within the entire stream + /// </summary> + /// <returns>A <see cref="ReadOnlySpan{T}"/> of the data within the entire stream</returns> + /// <exception cref="OverflowException"></exception> + public ReadOnlySpan<byte> AsSpan() + { + ReadOnlySpan<byte> output = _buffer.Span; + return output[..(int)_length]; + } + + /// <summary> + /// If the current stream is a readonly stream, creates a shallow copy for reading only. + /// </summary> + /// <returns>New stream shallow copy of the internal stream</returns> + /// <exception cref="InvalidOperationException"></exception> + public object Clone() => GetReadonlyShallowCopy(); + + /* + * Override the Dispose async method to avoid the base class overhead + * and task allocation since this will always be a syncrhonous + * operation (freeing memory) + */ + + ///<inheritdoc/> + public override ValueTask DisposeAsync() + { + //Dispose and return completed task + base.Dispose(true); + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } + } +}
\ No newline at end of file diff --git a/Utils/src/IO/VnStreamReader.cs b/Utils/src/IO/VnStreamReader.cs new file mode 100644 index 0000000..70b9734 --- /dev/null +++ b/Utils/src/IO/VnStreamReader.cs @@ -0,0 +1,180 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnStreamReader.cs +* +* VnStreamReader.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; +using System.IO; +using System.Text; +using System.Buffers; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Binary based buffered text reader, optimized for reading network streams + /// </summary> + public class VnStreamReader : TextReader, IVnTextReader + { + private bool disposedValue; + + private readonly ISlindingWindowBuffer<byte> _buffer; + ///<inheritdoc/> + public virtual Stream BaseStream { get; } + ///<inheritdoc/> + public Encoding Encoding { get; } + + /// <summary> + /// Number of available bytes of buffered data within the current buffer window + /// </summary> + public int Available => _buffer.AccumulatedSize; + /// <summary> + /// Gets or sets the line termination used to deliminate a line of data + /// </summary> + public ReadOnlyMemory<byte> LineTermination { get; set; } + Span<byte> IVnTextReader.BufferedDataWindow => _buffer.Accumulated; + + /// <summary> + /// Creates a new <see cref="TextReader"/> that reads encoded data from the base. + /// Internal buffers will be alloced from <see cref="ArrayPool{T}.Shared"/> + /// </summary> + /// <param name="baseStream">The underlying stream to read data from</param> + /// <param name="enc">The <see cref="Encoding"/> to use when reading from the stream</param> + /// <param name="bufferSize">The size of the internal binary buffer</param> + public VnStreamReader(Stream baseStream, Encoding enc, int bufferSize) + { + BaseStream = baseStream; + Encoding = enc; + //Init a new buffer + _buffer = InitializeBuffer(bufferSize); + } + + /// <summary> + /// Invoked by the constuctor method to allocte the internal buffer with the specified buffer size. + /// </summary> + /// <param name="bufferSize">The requested size of the buffer to alloc</param> + /// <remarks>By default requests the buffer from the <see cref="ArrayPool{T}.Shared"/> instance</remarks> + protected virtual ISlindingWindowBuffer<byte> InitializeBuffer(int bufferSize) => new ArrayPoolStreamBuffer<byte>(ArrayPool<byte>.Shared, bufferSize); + + ///<inheritdoc/> + public override async Task<string?> ReadLineAsync() + { + //If buffered data is available, check for line termination + if (Available > 0) + { + //Get current buffer window + Memory<byte> buffered = _buffer.AccumulatedBuffer; + //search for line termination in current buffer + int term = buffered.IndexOf(LineTermination); + //Termination found in buffer window + if (term > -1) + { + //Capture the line from the begining of the window to the termination + Memory<byte> line = buffered[..term]; + //Shift the window to the end of the line (excluding the termination) + _buffer.AdvanceStart(term + LineTermination.Length); + //Decode the line to a string + return Encoding.GetString(line.Span); + } + //Termination not found + } + //Compact the buffer window and see if space is avialble to buffer more data + if (_buffer.CompactBufferWindow()) + { + //There is room, so buffer more data + await _buffer.AccumulateDataAsync(BaseStream, CancellationToken.None); + //Check again to see if more data is buffered + if (Available <= 0) + { + //No string found + return null; + } + //Get current buffer window + Memory<byte> buffered = _buffer.AccumulatedBuffer; + //search for line termination in current buffer + int term = buffered.IndexOf(LineTermination); + //Termination found in buffer window + if (term > -1) + { + //Capture the line from the begining of the window to the termination + Memory<byte> line = buffered[..term]; + //Shift the window to the end of the line (excluding the termination) + _buffer.AdvanceStart(term + LineTermination.Length); + //Decode the line to a string + return Encoding.GetString(line.Span); + } + } + //Termination not found within the entire buffer, so buffer space has been exhausted + + //OOM is raised in the TextReader base class, the standard is preserved +#pragma warning disable CA2201 // Do not raise reserved exception types + throw new OutOfMemoryException("A line termination was not found within the buffer"); +#pragma warning restore CA2201 // Do not raise reserved exception types + } + + ///<inheritdoc/> + public override int Read(char[] buffer, int index, int count) => Read(buffer.AsSpan(index, count)); + ///<inheritdoc/> + public override int Read(Span<char> buffer) + { + if (Available <= 0) + { + return 0; + } + //Get current buffer window + Span<byte> buffered = _buffer.Accumulated; + //Convert all avialable data + int encoded = Encoding.GetChars(buffered, buffer); + //Shift buffer window to the end of the converted data + _buffer.AdvanceStart(encoded); + //return the number of chars written + return Encoding.GetCharCount(buffered); + } + ///<inheritdoc/> + public override void Close() => _buffer.Close(); + ///<inheritdoc/> + protected override void Dispose(bool disposing) + { + if (!disposedValue) + { + Close(); + disposedValue = true; + } + base.Dispose(disposing); + } + + /// <summary> + /// Resets the internal buffer window + /// </summary> + protected void ClearBuffer() + { + _buffer.Reset(); + } + + void IVnTextReader.Advance(int count) => _buffer.AdvanceStart(count); + void IVnTextReader.FillBuffer() => _buffer.AccumulateData(BaseStream); + ERRNO IVnTextReader.CompactBufferWindow() => _buffer.CompactBufferWindow(); + } +}
\ No newline at end of file diff --git a/Utils/src/IO/VnStreamWriter.cs b/Utils/src/IO/VnStreamWriter.cs new file mode 100644 index 0000000..37d700c --- /dev/null +++ b/Utils/src/IO/VnStreamWriter.cs @@ -0,0 +1,292 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnStreamWriter.cs +* +* VnStreamWriter.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; +using System.IO; +using System.Text; +using System.Buffers; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +using VNLib.Utils.Memory; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Provides a memory optimized <see cref="TextWriter"/> implementation. Optimized for writing + /// to network streams + /// </summary> + public class VnStreamWriter : TextWriter + { + private readonly Encoder Enc; + + private readonly ISlindingWindowBuffer<byte> _buffer; + + private bool closed; + + /// <summary> + /// Gets the underlying stream that interfaces with the backing store + /// </summary> + public virtual Stream BaseStream { get; } + ///<inheritdoc/> + public override Encoding Encoding { get; } + + /// <summary> + /// Line termination to use when writing lines to the output + /// </summary> + public ReadOnlyMemory<byte> LineTermination { get; set; } + ///<inheritdoc/> + public override string NewLine + { + get => Encoding.GetString(LineTermination.Span); + set => LineTermination = Encoding.GetBytes(value); + } + + /// <summary> + /// Creates a new <see cref="VnStreamWriter"/> that writes formatted data + /// to the specified base stream + /// </summary> + /// <param name="baseStream">The stream to write data to</param> + /// <param name="encoding">The <see cref="Encoding"/> to use when writing data</param> + /// <param name="bufferSize">The size of the internal buffer used to buffer binary data before writing to the base stream</param> + public VnStreamWriter(Stream baseStream, Encoding encoding, int bufferSize = 1024) + { + //Store base stream + BaseStream = baseStream; + Encoding = encoding; + //Get an encoder + Enc = encoding.GetEncoder(); + _buffer = InitializeBuffer(bufferSize); + } + + /// <summary> + /// Invoked by the constuctor method to allocte the internal buffer with the specified buffer size. + /// </summary> + /// <param name="bufferSize">The requested size of the buffer to alloc</param> + /// <remarks>By default requests the buffer from the <see cref="MemoryPool{T}.Shared"/> instance</remarks> + protected virtual ISlindingWindowBuffer<byte> InitializeBuffer(int bufferSize) => new ArrayPoolStreamBuffer<byte>(ArrayPool<byte>.Shared, bufferSize); + ///<inheritdoc/> + public void Write(byte value) + { + //See if there is room in the binary buffer + if (_buffer.AccumulatedSize == 0) + { + //There is not enough room to store the single byte + Flush(); + } + //Store at the end of the window + _buffer.Append(value); + } + ///<inheritdoc/> + public override void Write(char value) + { + ReadOnlySpan<char> tbuf = MemoryMarshal.CreateSpan(ref value, 0x01); + Write(tbuf); + } + ///<inheritdoc/> + public override void Write(object value) => Write(value.ToString()); + ///<inheritdoc/> + public override void Write(string value) => Write(value.AsSpan()); + ///<inheritdoc/> + public override void Write(ReadOnlySpan<char> buffer) + { + Check(); + + ForwardOnlyReader<char> reader = new(buffer); + + //Create a variable for a character buffer window + bool completed; + do + { + //Get an available buffer window to store characters in and convert the characters to binary + Enc.Convert(reader.Window, _buffer.Remaining, true, out int charsUsed, out int bytesUsed, out completed); + //Update byte position + _buffer.Advance(bytesUsed); + //Update char position + reader.Advance(charsUsed); + + //Converting did not complete because the buffer was too small + if (!completed || reader.WindowSize == 0) + { + //Flush the buffer and continue + Flush(); + } + + } while (!completed); + //Reset the encoder + Enc.Reset(); + } + ///<inheritdoc/> + public override async Task WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = default) + { + Check(); + //Create a variable for a character buffer window + bool completed; + ForwardOnlyMemoryReader<char> reader = new(buffer); + do + { + //Get an available buffer window to store characters in and convert the characters to binary + Enc.Convert(reader.Window.Span, _buffer.Remaining, true, out int charsUsed, out int bytesUsed, out completed); + //Update byte position + _buffer.Advance(bytesUsed); + //Update char position + reader.Advance(charsUsed); + //Converting did not complete because the buffer was too small + if (!completed || reader.WindowSize == 0) + { + //Flush the buffer and continue + await FlushWriterAsync(cancellationToken); + } + } while (!completed); + //Reset the encoder + Enc.Reset(); + } + + ///<inheritdoc/> + public override void WriteLine() + { + Check(); + //See if there is room in the binary buffer + if (_buffer.RemainingSize < LineTermination.Length) + { + //There is not enough room to store the termination, so we need to flush the buffer + Flush(); + } + _buffer.Append(LineTermination.Span); + } + ///<inheritdoc/> + public override void WriteLine(object value) => WriteLine(value.ToString()); + ///<inheritdoc/> + public override void WriteLine(string value) => WriteLine(value.AsSpan()); + ///<inheritdoc/> + public override void WriteLine(ReadOnlySpan<char> buffer) + { + //Write the value itself + Write(buffer); + //Write the line termination + WriteLine(); + } + + ///<inheritdoc/> + ///<exception cref="ObjectDisposedException"></exception> + public override void Flush() + { + Check(); + //If data is available to be written, write it to the base stream + if (_buffer.AccumulatedSize > 0) + { + //Write all buffered data to stream + BaseStream.Write(_buffer.Accumulated); + //Reset the buffer + _buffer.Reset(); + } + } + /// <summary> + /// Asynchronously flushes the internal buffers to the <see cref="BaseStream"/>, and resets the internal buffer state + /// </summary> + /// <returns>A <see cref="ValueTask"/> that represents the asynchronous flush operation</returns> + /// <exception cref="ObjectDisposedException"></exception> + public async ValueTask FlushWriterAsync(CancellationToken cancellationToken = default) + { + Check(); + if (_buffer.AccumulatedSize > 0) + { + //Flush current window to the stream + await BaseStream.WriteAsync(_buffer.AccumulatedBuffer, cancellationToken); + //Reset the buffer + _buffer.Reset(); + } + } + + ///<inheritdoc/> + public override Task FlushAsync() => FlushWriterAsync().AsTask(); + + /// <summary> + /// Resets internal properies for resuse + /// </summary> + protected void Reset() + { + _buffer.Reset(); + Enc.Reset(); + } + ///<inheritdoc/> + public override void Close() + { + //Only invoke close once + if (closed) + { + return; + } + try + { + Flush(); + } + finally + { + //Release the memory handle if its set + _buffer.Close(); + //Set closed flag + closed = true; + } + } + ///<inheritdoc/> + protected override void Dispose(bool disposing) + { + Close(); + base.Dispose(disposing); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Check() + { + if (closed) + { + throw new ObjectDisposedException("The stream is closed"); + } + } + ///<inheritdoc/> + public override async ValueTask DisposeAsync() + { + //Only invoke close once + if (closed) + { + return; + } + try + { + await FlushWriterAsync(); + } + finally + { + //Set closed flag + closed = true; + //Release the memory handle if its set + _buffer.Close(); + } + GC.SuppressFinalize(this); + } + } +}
\ No newline at end of file diff --git a/Utils/src/IO/VnTextReaderExtensions.cs b/Utils/src/IO/VnTextReaderExtensions.cs new file mode 100644 index 0000000..119461b --- /dev/null +++ b/Utils/src/IO/VnTextReaderExtensions.cs @@ -0,0 +1,223 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnTextReaderExtensions.cs +* +* VnTextReaderExtensions.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; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Extension methods to help reuse code for used TextReader implementations + /// </summary> + public static class VnTextReaderExtensions + { + public const int E_BUFFER_TOO_SMALL = -1; + + + /* + * Generic extensions provide constained compiler method invocation + * for structs the implement the IVNtextReader + */ + + /// <summary> + /// Attempts to read a line from the stream and store it in the specified buffer + /// </summary> + /// <param name="reader"></param> + /// <param name="charBuffer">The character buffer to write data to</param> + /// <returns>Returns the number of bytes read, <see cref="E_BUFFER_TOO_SMALL"/> + /// if the buffer was not large enough, 0 if no data was available</returns> + /// <exception cref="OutOfMemoryException"></exception> + /// <remarks>Allows reading lines of data from the stream without allocations</remarks> + public static ERRNO ReadLine<T>(this ref T reader, Span<char> charBuffer) where T:struct, IVnTextReader + { + return readLine(ref reader, charBuffer); + } + /// <summary> + /// Attempts to read a line from the stream and store it in the specified buffer + /// </summary> + /// <param name="reader"></param> + /// <param name="charBuffer">The character buffer to write data to</param> + /// <returns>Returns the number of bytes read, <see cref="E_BUFFER_TOO_SMALL"/> + /// if the buffer was not large enough, 0 if no data was available</returns> + /// <exception cref="OutOfMemoryException"></exception> + /// <remarks>Allows reading lines of data from the stream without allocations</remarks> + public static ERRNO ReadLine<T>(this T reader, Span<char> charBuffer) where T : class, IVnTextReader + { + return readLine(ref reader, charBuffer); + } + + /// <summary> + /// Fill a buffer with reamining buffered data + /// </summary> + /// <param name="reader"></param> + /// <param name="buffer">Buffer to copy data to</param> + /// <param name="offset">Offset in buffer to begin writing</param> + /// <param name="count">Number of bytes to read</param> + /// <returns>The number of bytes copied to the input buffer</returns> + public static int ReadRemaining<T>(this ref T reader, byte[] buffer, int offset, int count) where T : struct, IVnTextReader + { + return reader.ReadRemaining(buffer.AsSpan(offset, count)); + } + /// <summary> + /// Fill a buffer with reamining buffered data + /// </summary> + /// <param name="reader"></param> + /// <param name="buffer">Buffer to copy data to</param> + /// <param name="offset">Offset in buffer to begin writing</param> + /// <param name="count">Number of bytes to read</param> + /// <returns>The number of bytes copied to the input buffer</returns> + public static int ReadRemaining<T>(this T reader, byte[] buffer, int offset, int count) where T : class, IVnTextReader + { + return reader.ReadRemaining(buffer.AsSpan(offset, count)); + } + + /// <summary> + /// Fill a buffer with reamining buffered data, up to + /// the size of the supplied buffer + /// </summary> + /// <param name="reader"></param> + /// <param name="buffer">Buffer to copy data to</param> + /// <returns>The number of bytes copied to the input buffer</returns> + /// <remarks>You should use the <see cref="IVnTextReader.Available"/> property to know how much remaining data is buffered</remarks> + public static int ReadRemaining<T>(this ref T reader, Span<byte> buffer) where T : struct, IVnTextReader + { + return readRemaining(ref reader, buffer); + } + /// <summary> + /// Fill a buffer with reamining buffered data, up to + /// the size of the supplied buffer + /// </summary> + /// <param name="reader"></param> + /// <param name="buffer">Buffer to copy data to</param> + /// <returns>The number of bytes copied to the input buffer</returns> + /// <remarks>You should use the <see cref="IVnTextReader.Available"/> property to know how much remaining data is buffered</remarks> + public static int ReadRemaining<T>(this T reader, Span<byte> buffer) where T : class, IVnTextReader + { + return readRemaining(ref reader, buffer); + } + + private static ERRNO readLine<T>(ref T reader, Span<char> chars) where T: IVnTextReader + { + /* + * I am aware of a potential bug, the line decoding process + * shifts the interal buffer by the exact number of bytes to + * the end of the line, without considering if the decoder failed + * to properly decode the entire line. + * + * I dont expect this to be an issue unless there is a bug within the specified + * encoder implementation + */ + ReadOnlySpan<byte> LineTermination = reader.LineTermination.Span; + //If buffered data is available, check for line termination + if (reader.Available > 0) + { + //Get current buffer window + ReadOnlySpan<byte> bytes = reader.BufferedDataWindow; + //search for line termination in current buffer + int term = bytes.IndexOf(LineTermination); + //Termination found in buffer window + if (term > -1) + { + //Capture the line from the begining of the window to the termination + ReadOnlySpan<byte> line = bytes[..term]; + //Get the number ot chars + int charCount = reader.Encoding.GetCharCount(line); + //See if the buffer is large enough + if (bytes.Length < charCount) + { + return E_BUFFER_TOO_SMALL; + } + //Use the decoder to convert the data + _ = reader.Encoding.GetChars(line, chars); + //Shift the window to the end of the line (excluding the termination, regardless of the conversion result) + reader.Advance(term + LineTermination.Length); + //Return the number of characters + return charCount; + } + //Termination not found but there may be more data waiting + } + //Compact the buffer window and make sure it was compacted so there is room to fill the buffer + if (reader.CompactBufferWindow()) + { + //There is room, so buffer more data + reader.FillBuffer(); + //Check again to see if more data is buffered + if (reader.Available <= 0) + { + //No data avialable + return 0; + } + //Get current buffer window + ReadOnlySpan<byte> bytes = reader.BufferedDataWindow; + //search for line termination in current buffer + int term = bytes.IndexOf(LineTermination); + //Termination found in buffer window + if (term > -1) + { + //Capture the line from the begining of the window to the termination + ReadOnlySpan<byte> line = bytes[..term]; + //Get the number ot chars + int charCount = reader.Encoding.GetCharCount(line); + //See if the buffer is large enough + if (bytes.Length < charCount) + { + return E_BUFFER_TOO_SMALL; + } + //Use the decoder to convert the data + _ = reader.Encoding.GetChars(line, chars); + //Shift the window to the end of the line (excluding the termination, regardless of the conversion result) + reader.Advance(term + LineTermination.Length); + //Return the number of characters + return charCount; + } + } + + //Termination not found within the entire buffer, so buffer space has been exhausted + + //Supress as this response is expected when the buffer is exhausted, +#pragma warning disable CA2201 // Do not raise reserved exception types + throw new OutOfMemoryException("The line was not found within the current buffer, cannot continue"); +#pragma warning restore CA2201 // Do not raise reserved exception types + } + + private static int readRemaining<T>(ref T reader, Span<byte> buffer) where T: IVnTextReader + { + //guard for empty buffer + if (buffer.Length == 0 || reader.Available == 0) + { + return 0; + } + //get the remaining bytes in the reader + Span<byte> remaining = reader.BufferedDataWindow; + //Calculate the number of bytes to copy + int canCopy = Math.Min(remaining.Length, buffer.Length); + //Copy remaining bytes to buffer + remaining[..canCopy].CopyTo(buffer); + //Shift the window by the number of bytes copied + reader.Advance(canCopy); + return canCopy; + } + } +}
\ No newline at end of file diff --git a/Utils/src/IO/WriteOnlyBufferedStream.cs b/Utils/src/IO/WriteOnlyBufferedStream.cs new file mode 100644 index 0000000..5e7faa1 --- /dev/null +++ b/Utils/src/IO/WriteOnlyBufferedStream.cs @@ -0,0 +1,255 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: WriteOnlyBufferedStream.cs +* +* WriteOnlyBufferedStream.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; +using System.IO; +using System.Buffers; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils.Memory; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// A basic accumulator style write buffered stream + /// </summary> + public class WriteOnlyBufferedStream : Stream + { + private readonly ISlindingWindowBuffer<byte> _buffer; + private readonly bool LeaveOpen; + + /// <summary> + /// Gets the underlying stream that interfaces with the backing store + /// </summary> + public Stream BaseStream { get; init; } + + /// <summary> + /// Initalizes a new <see cref="WriteOnlyBufferedStream"/> using the + /// specified backing stream, using the specified buffer size, and + /// optionally leaves the stream open + /// </summary> + /// <param name="baseStream">The backing stream to write buffered data to</param> + /// <param name="bufferSize">The size of the internal buffer</param> + /// <param name="leaveOpen">A value indicating of the stream should be left open when the buffered stream is closed</param> + public WriteOnlyBufferedStream(Stream baseStream, int bufferSize, bool leaveOpen = false) + { + BaseStream = baseStream; + //Create buffer + _buffer = InitializeBuffer(bufferSize); + LeaveOpen = leaveOpen; + } + /// <summary> + /// Invoked by the constuctor method to allocte the internal buffer with the specified buffer size. + /// </summary> + /// <param name="bufferSize">The requested size of the buffer to alloc</param> + /// <remarks>By default requests the buffer from the <see cref="ArrayPool{T}.Shared"/> instance</remarks> + protected virtual ISlindingWindowBuffer<byte> InitializeBuffer(int bufferSize) + { + return new ArrayPoolStreamBuffer<byte>(ArrayPool<byte>.Shared, bufferSize); + } + + ///<inheritdoc/> + public override void Close() + { + try + { + //Make sure the buffer is empty + WriteBuffer(); + + if (!LeaveOpen) + { + //Dispose stream + BaseStream.Dispose(); + } + } + finally + { + _buffer.Close(); + } + } + ///<inheritdoc/> + public override async ValueTask DisposeAsync() + { + try + { + if (_buffer.AccumulatedSize > 0) + { + await WriteBufferAsync(CancellationToken.None); + } + + if (!LeaveOpen) + { + //Dispose stream + await BaseStream.DisposeAsync(); + } + + GC.SuppressFinalize(this); + } + finally + { + _buffer.Close(); + } + } + + ///<inheritdoc/> + public override void Flush() => WriteBuffer(); + ///<inheritdoc/> + public override Task FlushAsync(CancellationToken cancellationToken) => WriteBufferAsync(cancellationToken).AsTask(); + + private void WriteBuffer() + { + //Only if data is available to write + if (_buffer.AccumulatedSize > 0) + { + //Write data to stream + BaseStream.Write(_buffer.Accumulated); + //Reset position + _buffer.Reset(); + } + } + + private async ValueTask WriteBufferAsync(CancellationToken token = default) + { + if(_buffer.AccumulatedSize > 0) + { + await BaseStream.WriteAsync(_buffer.AccumulatedBuffer, token); + _buffer.Reset(); + } + } + ///<inheritdoc/> + public override void Write(byte[] buffer, int offset, int count) => Write(buffer.AsSpan(offset, count)); + + public override void Write(ReadOnlySpan<byte> buffer) + { + ForwardOnlyReader<byte> reader = new(buffer); + //Attempt to buffer/flush data until all data is sent + do + { + //Try to buffer as much as possible + ERRNO buffered = _buffer.TryAccumulate(reader.Window); + + if(buffered < reader.WindowSize) + { + //Buffer is full and needs to be flushed + WriteBuffer(); + //Advance reader and continue to buffer + reader.Advance(buffered); + continue; + } + + break; + } + while (true); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + } + + public async override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) + { + ForwardOnlyMemoryReader<byte> reader = new(buffer); + //Attempt to buffer/flush data until all data is sent + do + { + //Try to buffer as much as possible + ERRNO buffered = _buffer.TryAccumulate(reader.Window.Span); + + if (buffered < reader.WindowSize) + { + //Buffer is full and needs to be flushed + await WriteBufferAsync(cancellationToken); + //Advance reader and continue to buffer + reader.Advance(buffered); + continue; + } + + break; + } + while (true); + } + + + /// <summary> + /// Always false + /// </summary> + public override bool CanRead => false; + /// <summary> + /// Always returns false + /// </summary> + public override bool CanSeek => false; + /// <summary> + /// Always true + /// </summary> + public override bool CanWrite => true; + /// <summary> + /// Returns the size of the underlying buffer + /// </summary> + public override long Length => _buffer.AccumulatedSize; + /// <summary> + /// Always throws <see cref="NotSupportedException"/> + /// </summary> + /// <exception cref="NotSupportedException"></exception> + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + /// <summary> + /// Always throws <see cref="NotSupportedException"/> + /// </summary> + /// <exception cref="NotSupportedException"></exception> + /// <returns></returns> + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("This stream is not readable"); + } + + /// <summary> + /// Always throws <see cref="NotSupportedException"/> + /// </summary> + /// <exception cref="NotSupportedException"></exception> + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// <summary> + /// Always throws <see cref="NotSupportedException"/> + /// </summary> + /// <exception cref="NotSupportedException"></exception> + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/Utils/src/IObjectStorage.cs b/Utils/src/IObjectStorage.cs new file mode 100644 index 0000000..5c99cd8 --- /dev/null +++ b/Utils/src/IObjectStorage.cs @@ -0,0 +1,48 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IObjectStorage.cs +* +* IObjectStorage.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 +{ + /// <summary> + /// This object will provide methods for storing and retreiving objects by key-value pairing + /// </summary> + public interface IObjectStorage + { + /// <summary> + /// Attempts to retrieve the specified object from storage + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="key">Key for storage</param> + /// <returns>The object in storage, or T.default if object is not found</returns> + public T GetObject<T>(string key); + + /// <summary> + /// Stores the specified object with the specified key + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="key">Key paired with object</param> + /// <param name="obj">Object to store</param> + public void SetObject<T>(string key, T obj); + } +}
\ No newline at end of file diff --git a/Utils/src/Logging/ILogProvider.cs b/Utils/src/Logging/ILogProvider.cs new file mode 100644 index 0000000..55dbd6f --- /dev/null +++ b/Utils/src/Logging/ILogProvider.cs @@ -0,0 +1,79 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ILogProvider.cs +* +* ILogProvider.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.Logging +{ + /// <summary> + /// Self-contained logging interface that allows for applications events to be written to an + /// output source + /// </summary> + public interface ILogProvider + { + /// <summary> + /// Flushes any buffers to the output source + /// </summary> + abstract void Flush(); + + /// <summary> + /// Writes the string to the log with the specified priority log level + /// </summary> + /// <param name="level">The log priority level</param> + /// <param name="value">The message to print</param> + void Write(LogLevel level, string value); + /// <summary> + /// Writes the exception and optional string to the log with the specified priority log level + /// </summary> + /// <param name="level">The log priority level</param> + /// <param name="exception">An exception object to write</param> + /// <param name="value">The message to print</param> + void Write(LogLevel level, Exception exception, string value = ""); + /// <summary> + /// Writes the template string and params arguments to the log with the specified priority log level + /// </summary> + /// <param name="level">The log priority level</param> + /// <param name="value">The log template string</param> + /// <param name="args">Variable length array of objects to log with the specified templatre</param> + void Write(LogLevel level, string value, params object?[] args); + /// <summary> + /// Writes the template string and params arguments to the log with the specified priority log level + /// </summary> + /// <param name="level">The log priority level</param> + /// <param name="value">The log template string</param> + /// <param name="args">Variable length array of objects to log with the specified templatre</param> + void Write(LogLevel level, string value, params ValueType[] args); + + /// <summary> + /// Gets the underlying log source + /// </summary> + /// <returns>The underlying log source</returns> + object GetLogProvider(); + /// <summary> + /// Gets the underlying log source + /// </summary> + /// <returns>The underlying log source</returns> + public virtual T GetLogProvider<T>() => (T)GetLogProvider(); + } +} diff --git a/Utils/src/Logging/LogLevel.cs b/Utils/src/Logging/LogLevel.cs new file mode 100644 index 0000000..1851c26 --- /dev/null +++ b/Utils/src/Logging/LogLevel.cs @@ -0,0 +1,33 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: LogLevel.cs +* +* LogLevel.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.Logging +{ + public enum LogLevel + { + Verbose, Debug, Information, Warning, Error, Fatal + } +} diff --git a/Utils/src/Logging/LoggerExtensions.cs b/Utils/src/Logging/LoggerExtensions.cs new file mode 100644 index 0000000..cd314ed --- /dev/null +++ b/Utils/src/Logging/LoggerExtensions.cs @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: LoggerExtensions.cs +* +* LoggerExtensions.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/. +*/ + +#pragma warning disable CA1062 // Validate arguments of public methods + +using System; + +namespace VNLib.Utils.Logging +{ + /// <summary> + /// Extension helper methods for writing logs to a <see cref="ILogProvider"/> + /// </summary> + public static class LoggerExtensions + { + public static void Debug(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Debug, exp, value); + public static void Debug(this ILogProvider log, string value) => log.Write(LogLevel.Debug, value); + public static void Debug(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Debug, format, args); + public static void Debug(this ILogProvider log, string format, params ValueType[] args) => log.Write(LogLevel.Debug, format, args); + public static void Error(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Error, exp, value); + public static void Error(this ILogProvider log, string value) => log.Write(LogLevel.Error, value); + public static void Error(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Error, format, args); + public static void Fatal(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Fatal, exp, value); + public static void Fatal(this ILogProvider log, string value) => log.Write(LogLevel.Fatal, value); + public static void Fatal(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Fatal, format, args); + public static void Fatal(this ILogProvider log, string format, params ValueType[] args) => log.Write(LogLevel.Fatal, format, args); + public static void Information(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Information, exp, value); + public static void Information(this ILogProvider log, string value) => log.Write(LogLevel.Information, value); + public static void Information(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Information, format, args); + public static void Information(this ILogProvider log, string format, params ValueType[] args) => log.Write(LogLevel.Information, format, args); + public static void Verbose(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Verbose, exp, value); + public static void Verbose(this ILogProvider log, string value) => log.Write(LogLevel.Verbose, value); + public static void Verbose(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Verbose, format, args); + public static void Verbose(this ILogProvider log, string format, params ValueType[] args) => log.Write(LogLevel.Verbose, format, args); + public static void Warn(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Warning, exp, value); + public static void Warn(this ILogProvider log, string value) => log.Write(LogLevel.Warning, value); + public static void Warn(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Warning, format, args); + public static void Warn(this ILogProvider log, string format, params ValueType[] args) => log.Write(LogLevel.Warning, format, args); + } +} diff --git a/Utils/src/Memory/Caching/ICacheHolder.cs b/Utils/src/Memory/Caching/ICacheHolder.cs new file mode 100644 index 0000000..19eee64 --- /dev/null +++ b/Utils/src/Memory/Caching/ICacheHolder.cs @@ -0,0 +1,45 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ICacheHolder.cs +* +* ICacheHolder.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.Caching +{ + /// <summary> + /// Exposes basic control of classes that manage private caches + /// </summary> + public interface ICacheHolder + { + /// <summary> + /// Clears all held caches without causing application stopping effects. + /// </summary> + /// <remarks>This is a safe "light" cache clear</remarks> + void CacheClear(); + /// <summary> + /// Performs all necessary actions to clear all held caches immediatly. + /// </summary> + /// <remarks>A "hard" cache clear/reset regardless of cost</remarks> + void CacheHardClear(); + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/Caching/ICacheable.cs b/Utils/src/Memory/Caching/ICacheable.cs new file mode 100644 index 0000000..37575cc --- /dev/null +++ b/Utils/src/Memory/Caching/ICacheable.cs @@ -0,0 +1,44 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ICacheable.cs +* +* ICacheable.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.Caching +{ + /// <summary> + /// Represents a cacheable entity with an expiration + /// </summary> + public interface ICacheable : IEquatable<ICacheable> + { + /// <summary> + /// A <see cref="DateTime"/> value that the entry is no longer valid + /// </summary> + DateTime Expires { get; set; } + + /// <summary> + /// Invoked when a collection occurs + /// </summary> + void Evicted(); + } +} diff --git a/Utils/src/Memory/Caching/IObjectRental.cs b/Utils/src/Memory/Caching/IObjectRental.cs new file mode 100644 index 0000000..d9489f4 --- /dev/null +++ b/Utils/src/Memory/Caching/IObjectRental.cs @@ -0,0 +1,47 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IObjectRental.cs +* +* IObjectRental.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.Memory.Caching +{ + + /// <summary> + /// A thread safe store for reusing CLR managed objects + /// </summary> + /// <typeparam name="T">The reusable object class</typeparam> + public interface IObjectRental<T> where T: class + { + /// <summary> + /// Gets an object from the store, or creates a new one if none are available + /// </summary> + /// <returns>An instance of <typeparamref name="T"/> from the store if available or a new instance if none were available</returns> + T Rent(); + + /// <summary> + /// Returns a rented object back to the rental store for reuse + /// </summary> + /// <param name="item">The previously rented item</param> + void Return(T item); + } + +}
\ No newline at end of file diff --git a/Utils/src/Memory/Caching/IReusable.cs b/Utils/src/Memory/Caching/IReusable.cs new file mode 100644 index 0000000..618878f --- /dev/null +++ b/Utils/src/Memory/Caching/IReusable.cs @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IReusable.cs +* +* IReusable.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.Memory.Caching +{ + /// <summary> + /// Allows for use within a <see cref="ReusableStore{T}"/>, this object is intended to be reused heavily + /// </summary> + public interface IReusable + { + /// <summary> + /// The instance should prepare itself for use (or re-use) + /// </summary> + void Prepare(); + /// <summary> + /// The intance is being returned and should determine if it's state is reusabled + /// </summary> + /// <returns>true if the instance can/should be reused, false if it should not be reused</returns> + bool Release(); + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/Caching/LRUCache.cs b/Utils/src/Memory/Caching/LRUCache.cs new file mode 100644 index 0000000..7e96e0a --- /dev/null +++ b/Utils/src/Memory/Caching/LRUCache.cs @@ -0,0 +1,127 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: LRUCache.cs +* +* LRUCache.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.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace VNLib.Utils.Memory.Caching +{ + /// <summary> + /// A base class for a Least Recently Used cache + /// </summary> + /// <typeparam name="TKey">The key for O(1) lookups</typeparam> + /// <typeparam name="TValue">The value to store within cache</typeparam> + public abstract class LRUCache<TKey, TValue> : LRUDataStore<TKey, TValue> where TKey : notnull + { + ///<inheritdoc/> + protected LRUCache() + {} + ///<inheritdoc/> + protected LRUCache(int initialCapacity) : base(initialCapacity) + {} + ///<inheritdoc/> + protected LRUCache(IEqualityComparer<TKey> keyComparer) : base(keyComparer) + {} + ///<inheritdoc/> + protected LRUCache(int initialCapacity, IEqualityComparer<TKey> keyComparer) : base(initialCapacity, keyComparer) + {} + + /// <summary> + /// The maximum number of items to store in LRU cache + /// </summary> + protected abstract int MaxCapacity { get; } + + /// <summary> + /// Adds a new record to the LRU cache + /// </summary> + /// <param name="item">A <see cref="KeyValuePair{TKey, TValue}"/> to add to the cache store</param> + public override void Add(KeyValuePair<TKey, TValue> item) + { + //See if the store is at max capacity and an item needs to be evicted + if(Count == MaxCapacity) + { + //A record needs to be evicted before a new record can be added + + //Get the oldest node from the list to reuse its instance and remove the old value + LinkedListNode<KeyValuePair<TKey, TValue>> oldNode = List.First!; //not null because count is at max capacity so an item must be at the end of the list + //Store old node value field + KeyValuePair<TKey, TValue> oldRecord = oldNode.Value; + //Remove from lookup + LookupTable.Remove(oldRecord.Key); + //Remove the node + List.RemoveFirst(); + //Reuse the old ll node + oldNode.Value = item; + //add lookup with new key + LookupTable.Add(item.Key, oldNode); + //Add to end of list + List.AddLast(oldNode); + //Invoke evicted method + Evicted(oldRecord); + } + else + { + //Add new item to the list + base.Add(item); + } + } + /// <summary> + /// Attempts to get a value by the given key. + /// </summary> + /// <param name="key">The key identifying the value to store</param> + /// <param name="value">The value to store</param> + /// <returns>A value indicating if the value was found in the store</returns> + public override bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) + { + //See if the cache contains the value + if(base.TryGetValue(key, out value)) + { + //Cache hit + return true; + } + //Cache miss + if(CacheMiss(key, out value)) + { + //Lookup hit + //Add the record to the store (eviction will happen as necessary + Add(key, value); + return true; + } + //Record does not exist + return false; + } + /// <summary> + /// Invoked when a record is evicted from the cache + /// </summary> + /// <param name="evicted">The record that is being evicted</param> + protected abstract void Evicted(KeyValuePair<TKey, TValue> evicted); + /// <summary> + /// Invoked when an entry was requested and was not found in cache. + /// </summary> + /// <param name="key">The key identifying the record to lookup</param> + /// <param name="value">The found value matching the key</param> + /// <returns>A value indicating if the record was found</returns> + protected abstract bool CacheMiss(TKey key, [NotNullWhen(true)] out TValue? value); + } +} diff --git a/Utils/src/Memory/Caching/LRUDataStore.cs b/Utils/src/Memory/Caching/LRUDataStore.cs new file mode 100644 index 0000000..f564fcc --- /dev/null +++ b/Utils/src/Memory/Caching/LRUDataStore.cs @@ -0,0 +1,232 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: LRUDataStore.cs +* +* LRUDataStore.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; +using System.Linq; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace VNLib.Utils.Memory.Caching +{ + /// <summary> + /// A Least Recently Used store base class for E2E O(1) operations + /// </summary> + /// <typeparam name="TKey">A key used for O(1) lookups</typeparam> + /// <typeparam name="TValue">A value to store</typeparam> + public abstract class LRUDataStore<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IReadOnlyCollection<TValue>, IEnumerable<KeyValuePair<TKey, TValue>> + where TKey: notnull + { + /// <summary> + /// A lookup table that provides O(1) access times for key-value lookups + /// </summary> + protected Dictionary<TKey, LinkedListNode<KeyValuePair<TKey, TValue>>> LookupTable { get; } + /// <summary> + /// A linked list that tracks the least recently used item. + /// New items (or recently access items) are moved to the end of the list. + /// The head contains the least recently used item + /// </summary> + protected LinkedList<KeyValuePair<TKey, TValue>> List { get; } + + /// <summary> + /// Initializes an empty <see cref="LRUDataStore{TKey, TValue}"/> + /// </summary> + protected LRUDataStore() + { + LookupTable = new(); + List = new(); + } + /// <summary> + /// Initializes an empty <see cref="LRUDataStore{TKey, TValue}"/> and sets + /// the lookup table's inital capacity + /// </summary> + /// <param name="initialCapacity">LookupTable initial capacity</param> + protected LRUDataStore(int initialCapacity) + { + LookupTable = new(initialCapacity); + List = new(); + } + /// <summary> + /// Initializes an empty <see cref="LRUDataStore{TKey, TValue}"/> and uses the + /// specified keycomparison + /// </summary> + /// <param name="keyComparer">A <see cref="IEqualityComparer{T}"/> used by the Lookuptable to compare keys</param> + protected LRUDataStore(IEqualityComparer<TKey> keyComparer) + { + LookupTable = new(keyComparer); + List = new(); + } + /// <summary> + /// Initializes an empty <see cref="LRUDataStore{TKey, TValue}"/> and uses the + /// specified keycomparison, and sets the lookup table's initial capacity + /// </summary> + /// <param name="initialCapacity">LookupTable initial capacity</param> + /// <param name="keyComparer">A <see cref="IEqualityComparer{T}"/> used by the Lookuptable to compare keys</param> + protected LRUDataStore(int initialCapacity, IEqualityComparer<TKey> keyComparer) + { + LookupTable = new(initialCapacity, keyComparer); + List = new(); + } + + /// <summary> + /// Gets or sets a value within the LRU cache. + /// </summary> + /// <param name="key">The key identifying the value</param> + /// <returns>The value stored at the given key</returns> + /// <remarks>Items are promoted in the store when accessed</remarks> + public virtual TValue this[TKey key] + { + get + { + return TryGetValue(key, out TValue? value) + ? value + : throw new KeyNotFoundException("The item or its key were not found in the LRU data store"); + } + set + { + //If a node by the same key in the store exists, just replace its value + if(LookupTable.TryGetValue(key, out LinkedListNode<KeyValuePair<TKey, TValue>>? oldNode)) + { + //Remove the node before re-adding it + List.Remove(oldNode); + oldNode.Value = new KeyValuePair<TKey, TValue>(key, value); + //Move the item to the front of the list + List.AddLast(oldNode); + } + else + { + //Node does not exist yet so create new one + Add(key, value); + } + } + } + ///<inheritdoc/> + public ICollection<TKey> Keys => LookupTable.Keys; + ///<inheritdoc/> + ///<exception cref="NotImplementedException"></exception> + public ICollection<TValue> Values => throw new NotImplementedException(); + IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => LookupTable.Keys; + IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => List.Select(static node => node.Value); + IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() => List.Select(static node => node.Value).GetEnumerator(); + + /// <summary> + /// Gets the number of items within the LRU store + /// </summary> + public int Count => List.Count; + ///<inheritdoc/> + public abstract bool IsReadOnly { get; } + + /// <summary> + /// Adds the specified record to the store and places it at the end of the LRU queue + /// </summary> + /// <param name="key">The key identifying the record</param> + /// <param name="value">The value to store at the key</param> + public void Add(TKey key, TValue value) + { + //Create new kvp lookup ref + KeyValuePair<TKey, TValue> lookupRef = new(key, value); + //Insert the lookup + Add(lookupRef); + } + ///<inheritdoc/> + public bool Remove(KeyValuePair<TKey, TValue> item) => Remove(item.Key); + ///<inheritdoc/> + IEnumerator IEnumerable.GetEnumerator() => List.GetEnumerator(); + ///<inheritdoc/> + public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) => List.CopyTo(array, arrayIndex); + ///<inheritdoc/> + public virtual bool ContainsKey(TKey key) => LookupTable.ContainsKey(key); + ///<inheritdoc/> + public virtual IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => List.GetEnumerator(); + + /// <summary> + /// Adds the specified record to the store and places it at the end of the LRU queue + /// </summary> + /// <param name="item">The item to add</param> + public virtual void Add(KeyValuePair<TKey, TValue> item) + { + //Init new ll node + LinkedListNode<KeyValuePair<TKey, TValue>> newNode = new(item); + //Insert the new node + LookupTable.Add(item.Key, newNode); + //Add to the end of the linked list + List.AddLast(newNode); + } + /// <summary> + /// Removes all elements from the LRU store + /// </summary> + public virtual void Clear() + { + //Clear lists + LookupTable.Clear(); + List.Clear(); + } + /// <summary> + /// Determines if the <see cref="KeyValuePair{TKey, TValue}"/> exists in the store + /// </summary> + /// <param name="item">The record to search for</param> + /// <returns>True if the key was found in the store and the value equals the stored value, false otherwise</returns> + public virtual bool Contains(KeyValuePair<TKey, TValue> item) + { + if (LookupTable.TryGetValue(item.Key, out LinkedListNode<KeyValuePair<TKey, TValue>>? lookup)) + { + return lookup.Value.Value?.Equals(item.Value) ?? false; + } + return false; + } + ///<inheritdoc/> + public virtual bool Remove(TKey key) + { + //Remove the item from the lookup table and if it exists, remove the node from the list + if(LookupTable.Remove(key, out LinkedListNode<KeyValuePair<TKey, TValue>>? node)) + { + //Remove the new from the list + List.Remove(node); + return true; + } + return false; + } + /// <summary> + /// Tries to get a value from the store with its key. Found items are promoted + /// </summary> + /// <param name="key">The key identifying the value</param> + /// <param name="value">The found value</param> + /// <returns>A value indicating if the element was found in the store</returns> + public virtual bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) + { + //Lookup the + if (LookupTable.TryGetValue(key, out LinkedListNode<KeyValuePair<TKey, TValue>>? val)) + { + //Remove the value from the list and add it to the front of the list + List.Remove(val); + List.AddLast(val); + value = val.Value.Value!; + return true; + } + value = default; + return false; + } + + } +} diff --git a/Utils/src/Memory/Caching/ObjectRental.cs b/Utils/src/Memory/Caching/ObjectRental.cs new file mode 100644 index 0000000..22aca95 --- /dev/null +++ b/Utils/src/Memory/Caching/ObjectRental.cs @@ -0,0 +1,236 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ObjectRental.cs +* +* ObjectRental.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; +using System.Threading; +using System.Diagnostics; +using System.Collections; +using System.Collections.Generic; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.Memory.Caching +{ + //TODO: implement lock-free object tracking + + /// <summary> + /// Provides concurrent storage for reusable objects to be rented and returned. This class + /// and its members is thread-safe + /// </summary> + /// <typeparam name="T">The data type to reuse</typeparam> + public class ObjectRental<T> : ObjectRental, IObjectRental<T>, ICacheHolder, IEnumerable<T> where T: class + { + /// <summary> + /// The initial data-structure capacity if quota is not defined + /// </summary> + public const int INITIAL_STRUCTURE_SIZE = 50; + + protected readonly SemaphoreSlim StorageLock; + protected readonly Stack<T> Storage; + protected readonly HashSet<T> ContainsStore; + + protected readonly Action<T>? ReturnAction; + protected readonly Action<T>? RentAction; + protected readonly Func<T> Constructor; + /// <summary> + /// Is the object type in the current store implement the Idisposable interface? + /// </summary> + protected readonly bool IsDisposableType; + + /// <summary> + /// The maximum number of objects that will be cached. + /// Once this threshold has been reached, objects are + /// no longer stored + /// </summary> + protected readonly int QuotaLimit; + +#pragma warning disable CS8618 //Internal constructor does not set the constructor function + private ObjectRental(int quota) +#pragma warning restore CS8618 + { + //alloc new stack for rentals + Storage = new(Math.Max(quota, INITIAL_STRUCTURE_SIZE)); + //Hashtable for quick lookups + ContainsStore = new(Math.Max(quota, INITIAL_STRUCTURE_SIZE)); + //Semaphore slim to provide exclusive access + StorageLock = new SemaphoreSlim(1, 1); + //Store quota, if quota is -1, set to int-max to "disable quota" + QuotaLimit = quota == 0 ? int.MaxValue : quota; + //Determine if the type is disposeable and store a local value + IsDisposableType = typeof(IDisposable).IsAssignableFrom(typeof(T)); + } + + /// <summary> + /// Creates a new <see cref="ObjectRental{T}"/> store with the rent/return callback methods + /// </summary> + /// <param name="constructor">The type initializer</param> + /// <param name="rentCb">The pre-retnal preperation action</param> + /// <param name="returnCb">The pre-return cleanup action</param> + /// <param name="quota">The maximum number of elements to cache in the store</param> + protected internal ObjectRental(Func<T> constructor, Action<T>? rentCb, Action<T>? returnCb, int quota) : this(quota) + { + this.RentAction = rentCb; + this.ReturnAction = returnCb; + this.Constructor = constructor; + } + + /// <inheritdoc/> + /// <exception cref="ObjectDisposedException"></exception> + public virtual T Rent() + { + Check(); + //See if we have an available object, if not return a new one by invoking the constructor function + T? rental = default; + //Get lock + using (SemSlimReleaser releader = StorageLock.GetReleaser()) + { + //See if the store contains an item ready to use + if(Storage.TryPop(out T? item)) + { + rental = item; + //Remove the item from the hash table + ContainsStore.Remove(item); + } + } + //If no object was removed from the store, create a new one + rental ??= Constructor(); + //If rental cb is defined, invoke it + RentAction?.Invoke(rental); + return rental; + } + + /// <inheritdoc/> + /// <exception cref="ObjectDisposedException"></exception> + public virtual void Return(T item) + { + Check(); + //Invoke return callback if set + ReturnAction?.Invoke(item); + //Keeps track to know if the element was added or need to be cleaned up + bool wasAdded = false; + using (SemSlimReleaser releader = StorageLock.GetReleaser()) + { + //Check quota limit + if (Storage.Count < QuotaLimit) + { + //Store item if it doesnt exist already + if (ContainsStore.Add(item)) + { + //Store the object + Storage.Push(item); + } + //Set the was added flag + wasAdded = true; + } + } + if (!wasAdded && IsDisposableType) + { + //If the element was not added and is disposeable, we can dispose the element + (item as IDisposable)!.Dispose(); + //Write debug message + Debug.WriteLine("Object rental disposed an object over quota"); + } + } + + /// <remarks> + /// NOTE: If <typeparamref name="T"/> implements <see cref="IDisposable"/> + /// interface, this method does nothing + /// </remarks> + /// <inheritdoc/> + /// <exception cref="ObjectDisposedException"></exception> + public virtual void CacheClear() + { + Check(); + //If the type is disposeable, cleaning can be a long process, so defer to hard clear + if (IsDisposableType) + { + return; + } + //take the semaphore + using SemSlimReleaser releader = StorageLock.GetReleaser(); + //Clear stores + ContainsStore.Clear(); + Storage.Clear(); + } + + /// <inheritdoc/> + /// <exception cref="ObjectDisposedException"></exception> + public virtual void CacheHardClear() + { + Check(); + //take the semaphore + using SemSlimReleaser releader = StorageLock.GetReleaser(); + //If the type is disposable, dispose all elements before clearing storage + if (IsDisposableType) + { + //Dispose all elements + foreach (T element in Storage.ToArray()) + { + (element as IDisposable)!.Dispose(); + } + } + //Clear the storeage + Storage.Clear(); + ContainsStore.Clear(); + } + ///<inheritdoc/> + protected override void Free() + { + StorageLock.Dispose(); + //If the element type is disposable, dispose all elements on a hard clear + if (IsDisposableType) + { + //Get all elements + foreach (T element in Storage.ToArray()) + { + (element as IDisposable)!.Dispose(); + } + } + } + + ///<inheritdoc/> + public IEnumerator<T> GetEnumerator() + { + Check(); + //Enter the semaphore + using SemSlimReleaser releader = StorageLock.GetReleaser(); + foreach (T item in Storage) + { + yield return item; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + Check(); + //Enter the semaphore + using SemSlimReleaser releader = StorageLock.GetReleaser(); + foreach (T item in Storage) + { + yield return item; + } + } + } + +}
\ No newline at end of file diff --git a/Utils/src/Memory/Caching/ObjectRentalBase.cs b/Utils/src/Memory/Caching/ObjectRentalBase.cs new file mode 100644 index 0000000..305d93f --- /dev/null +++ b/Utils/src/Memory/Caching/ObjectRentalBase.cs @@ -0,0 +1,155 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ObjectRentalBase.cs +* +* ObjectRentalBase.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.Caching +{ + /// <summary> + /// Provides concurrent storage for reusable objects to be rented and returned. This class + /// and its members is thread-safe + /// </summary> + public abstract class ObjectRental : VnDisposeable + { + /// <summary> + /// Creates a new <see cref="ObjectRental{T}"/> store + /// </summary> + /// <param name="quota">The maximum number of elements that will be cached</param> + public static ObjectRental<TNew> Create<TNew>(int quota = 0) where TNew : class, new() + { + static TNew constructor() => new(); + return new ObjectRental<TNew>(constructor, null, null, quota); + } + /// <summary> + /// Creates a new <see cref="ObjectRental{T}"/> store with generic rental and return callback handlers + /// </summary> + /// <param name="rentCb">Function responsible for preparing an instance to be rented</param> + /// <param name="returnCb">Function responsible for cleaning up an instance before reuse</param> + /// <param name="quota">The maximum number of elements that will be cached</param> + public static ObjectRental<TNew> Create<TNew>(Action<TNew>? rentCb, Action<TNew>? returnCb, int quota = 0) where TNew : class, new() + { + static TNew constructor() => new(); + return new ObjectRental<TNew>(constructor, rentCb, returnCb, quota); + } + /// <summary> + /// Creates a new <see cref="ObjectRental{T}"/> store with a generic constructor function + /// </summary> + /// <param name="constructor">The function invoked to create a new instance when required</param> + /// <param name="quota">The maximum number of elements that will be cached</param> + /// <returns></returns> + public static ObjectRental<TNew> Create<TNew>(Func<TNew> constructor, int quota = 0) where TNew: class + { + return new ObjectRental<TNew>(constructor, null, null, quota); + } + /// <summary> + /// Creates a new <see cref="ObjectRental{T}"/> store with generic rental and return callback handlers + /// </summary> + /// <param name="constructor">The function invoked to create a new instance when required</param> + /// <param name="rentCb">Function responsible for preparing an instance to be rented</param> + /// <param name="returnCb">Function responsible for cleaning up an instance before reuse</param> + /// <param name="quota">The maximum number of elements that will be cached</param> + public static ObjectRental<TNew> Create<TNew>(Func<TNew> constructor, Action<TNew>? rentCb, Action<TNew>? returnCb, int quota = 0) where TNew : class + { + return new ObjectRental<TNew>(constructor, rentCb, returnCb, quota); + } + + /// <summary> + /// Creates a new <see cref="ThreadLocalObjectStorage{TNew}"/> store with generic rental and return callback handlers + /// </summary> + /// <typeparam name="TNew"></typeparam> + /// <param name="constructor">The function invoked to create a new instance when required</param> + /// <param name="rentCb">Function responsible for preparing an instance to be rented</param> + /// <param name="returnCb">Function responsible for cleaning up an instance before reuse</param> + /// <returns>The initialized store</returns> + public static ThreadLocalObjectStorage<TNew> CreateThreadLocal<TNew>(Func<TNew> constructor, Action<TNew>? rentCb, Action<TNew>? returnCb) where TNew : class + { + return new ThreadLocalObjectStorage<TNew>(constructor, rentCb, returnCb); + } + /// <summary> + /// Creates a new <see cref="ThreadLocalObjectStorage{T}"/> store with generic rental and return callback handlers + /// </summary> + /// <param name="rentCb">Function responsible for preparing an instance to be rented</param> + /// <param name="returnCb">Function responsible for cleaning up an instance before reuse</param> + public static ThreadLocalObjectStorage<TNew> CreateThreadLocal<TNew>(Action<TNew>? rentCb, Action<TNew>? returnCb) where TNew : class, new() + { + static TNew constructor() => new(); + return new ThreadLocalObjectStorage<TNew>(constructor, rentCb, returnCb); + } + /// <summary> + /// Creates a new <see cref="ThreadLocalObjectStorage{T}"/> store + /// </summary> + public static ThreadLocalObjectStorage<TNew> CreateThreadLocal<TNew>() where TNew : class, new() + { + static TNew constructor() => new(); + return new ThreadLocalObjectStorage<TNew>(constructor, null, null); + } + /// <summary> + /// Creates a new <see cref="ThreadLocalObjectStorage{T}"/> store with a generic constructor function + /// </summary> + /// <param name="constructor">The function invoked to create a new instance when required</param> + /// <returns></returns> + public static ThreadLocalObjectStorage<TNew> CreateThreadLocal<TNew>(Func<TNew> constructor) where TNew : class + { + return new ThreadLocalObjectStorage<TNew>(constructor, null, null); + } + + /// <summary> + /// Creates a new <see cref="ReusableStore{T}"/> instance with a parameterless constructor + /// </summary> + /// <typeparam name="T">The <see cref="IReusable"/> type</typeparam> + /// <param name="quota">The maximum number of elements that will be cached</param> + /// <returns></returns> + public static ReusableStore<T> CreateReusable<T>(int quota = 0) where T : class, IReusable, new() + { + static T constructor() => new(); + return new(constructor, quota); + } + /// <summary> + /// Creates a new <see cref="ReusableStore{T}"/> instance with the specified constructor + /// </summary> + /// <typeparam name="T">The <see cref="IReusable"/> type</typeparam> + /// <param name="constructor">The constructor function invoked to create new instances of the <see cref="IReusable"/> type</param> + /// <param name="quota">The maximum number of elements that will be cached</param> + /// <returns></returns> + public static ReusableStore<T> CreateReusable<T>(Func<T> constructor, int quota = 0) where T : class, IReusable => new(constructor, quota); + + /// <summary> + /// Creates a new <see cref="ThreadLocalReusableStore{T}"/> instance with a parameterless constructor + /// </summary> + /// <typeparam name="T">The <see cref="IReusable"/> type</typeparam> + /// <returns></returns> + public static ThreadLocalReusableStore<T> CreateThreadLocalReusable<T>() where T : class, IReusable, new() + { + static T constructor() => new(); + return new(constructor); + } + /// <summary> + /// Creates a new <see cref="ThreadLocalReusableStore{T}"/> instance with the specified constructor + /// </summary> + /// <typeparam name="T">The <see cref="IReusable"/> type</typeparam> + /// <param name="constructor">The constructor function invoked to create new instances of the <see cref="IReusable"/> type</param> + /// <returns></returns> + public static ThreadLocalReusableStore<T> CreateThreadLocalReusable<T>(Func<T> constructor) where T : class, IReusable => new(constructor); + } +} diff --git a/Utils/src/Memory/Caching/ReusableStore.cs b/Utils/src/Memory/Caching/ReusableStore.cs new file mode 100644 index 0000000..aacd012 --- /dev/null +++ b/Utils/src/Memory/Caching/ReusableStore.cs @@ -0,0 +1,61 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ReusableStore.cs +* +* ReusableStore.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.Caching +{ + + /// <summary> + /// A reusable object store that extends <see cref="ObjectRental{T}"/>, that allows for objects to be reused heavily + /// </summary> + /// <typeparam name="T">A reusable object</typeparam> + public class ReusableStore<T> : ObjectRental<T> where T : class, IReusable + { + internal ReusableStore(Func<T> constructor, int quota) :base(constructor, null, null, quota) + {} + ///<inheritdoc/> + public override T Rent() + { + //Rent the object (or create it) + T rental = base.Rent(); + //Invoke prepare function + rental.Prepare(); + //return object + return rental; + } + ///<inheritdoc/> + public override void Return(T item) + { + /* + * Clean up the item by invoking the cleanup function, + * and only return the item for reuse if the caller allows + */ + if (item.Release()) + { + base.Return(item); + } + } + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/Caching/ThreadLocalObjectStorage.cs b/Utils/src/Memory/Caching/ThreadLocalObjectStorage.cs new file mode 100644 index 0000000..511af24 --- /dev/null +++ b/Utils/src/Memory/Caching/ThreadLocalObjectStorage.cs @@ -0,0 +1,76 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ThreadLocalObjectStorage.cs +* +* ThreadLocalObjectStorage.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; +using System.Threading; + +namespace VNLib.Utils.Memory.Caching +{ + /// <summary> + /// Derrives from <see cref="ObjectRental{T}"/> to provide object rental syntax for <see cref="ThreadLocal{T}"/> + /// storage + /// </summary> + /// <typeparam name="T">The data type to store</typeparam> + public class ThreadLocalObjectStorage<T> : ObjectRental<T> where T: class + { + protected ThreadLocal<T> Store { get; } + + internal ThreadLocalObjectStorage(Func<T> constructor, Action<T>? rentCb, Action<T>? returnCb) + :base(constructor, rentCb, returnCb, 0) + { + Store = new(Constructor); + } + + /// <summary> + /// "Rents" or creates an object for the current thread + /// </summary> + /// <returns>The new or stored instanced</returns> + /// <exception cref="ObjectDisposedException"></exception> + public override T Rent() + { + Check(); + //Get the tlocal value + T value = Store.Value!; + //Invoke the rent action if set + base.RentAction?.Invoke(value); + return value; + } + + /// <inheritdoc/> + /// <exception cref="ObjectDisposedException"></exception> + public override void Return(T item) + { + Check(); + //Invoke the rent action + base.ReturnAction?.Invoke(item); + } + + ///<inheritdoc/> + protected override void Free() + { + Store.Dispose(); + base.Free(); + } + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/Caching/ThreadLocalReusableStore.cs b/Utils/src/Memory/Caching/ThreadLocalReusableStore.cs new file mode 100644 index 0000000..83cd4d6 --- /dev/null +++ b/Utils/src/Memory/Caching/ThreadLocalReusableStore.cs @@ -0,0 +1,64 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ThreadLocalReusableStore.cs +* +* ThreadLocalReusableStore.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.Caching +{ + /// <summary> + /// A reusable object store that extends <see cref="ThreadLocalObjectStorage{T}"/>, that allows for objects to be reused heavily + /// in a thread-local cache + /// </summary> + /// <typeparam name="T">A reusable object</typeparam> + public class ThreadLocalReusableStore<T> : ThreadLocalObjectStorage<T> where T: class, IReusable + { + /// <summary> + /// Creates a new <see cref="ThreadLocalReusableStore{T}"/> instance + /// </summary> + internal ThreadLocalReusableStore(Func<T> constructor):base(constructor, null, null) + { } + ///<inheritdoc/> + public override T Rent() + { + //Rent the object (or create it) + T rental = base.Rent(); + //Invoke prepare function + rental.Prepare(); + //return object + return rental; + } + ///<inheritdoc/> + public override void Return(T item) + { + /* + * Clean up the item by invoking the cleanup function, + * and only return the item for reuse if the caller allows + */ + if (item.Release()) + { + base.Return(item); + } + } + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/ForwardOnlyBufferWriter.cs b/Utils/src/Memory/ForwardOnlyBufferWriter.cs new file mode 100644 index 0000000..0ea507e --- /dev/null +++ b/Utils/src/Memory/ForwardOnlyBufferWriter.cs @@ -0,0 +1,122 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ForwardOnlyBufferWriter.cs +* +* ForwardOnlyBufferWriter.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> + /// Provides a stack based buffer writer + /// </summary> + public ref struct ForwardOnlyWriter<T> + { + /// <summary> + /// The buffer for writing output data to + /// </summary> + public readonly Span<T> Buffer { get; } + /// <summary> + /// The number of characters written to the buffer + /// </summary> + public int Written { readonly get; set; } + /// <summary> + /// The number of characters remaining in the buffer + /// </summary> + public readonly int RemainingSize => Buffer.Length - Written; + + /// <summary> + /// The remaining buffer window + /// </summary> + public readonly Span<T> Remaining => Buffer[Written..]; + + /// <summary> + /// Creates a new <see cref="ForwardOnlyWriter{T}"/> assigning the specified buffer + /// </summary> + /// <param name="buffer">The buffer to write data to</param> + public ForwardOnlyWriter(in Span<T> buffer) + { + Buffer = buffer; + Written = 0; + } + + /// <summary> + /// Returns a compiled string from the characters written to the buffer + /// </summary> + /// <returns>A string of the characters written to the buffer</returns> + public readonly override string ToString() => Buffer[..Written].ToString(); + + /// <summary> + /// Appends a sequence to the buffer + /// </summary> + /// <param name="data">The data to append to the buffer</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public void Append(ReadOnlySpan<T> data) + { + //Make sure the current window is large enough to buffer the new string + if (data.Length > RemainingSize) + { + throw new ArgumentOutOfRangeException(nameof(Remaining) ,"The internal buffer does not have enough buffer space"); + } + Span<T> window = Buffer[Written..]; + //write data to window + data.CopyTo(window); + //update char position + Written += data.Length; + } + /// <summary> + /// Appends a single item to the buffer + /// </summary> + /// <param name="c">The item to append to the buffer</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public void Append(T c) + { + //Make sure the current window is large enough to buffer the new string + if (RemainingSize == 0) + { + throw new ArgumentOutOfRangeException(nameof(Remaining), "The internal buffer does not have enough buffer space"); + } + //Write data to buffer and increment the buffer position + Buffer[Written++] = c; + } + + /// <summary> + /// Advances the writer forward the specifed number of elements + /// </summary> + /// <param name="count">The number of elements to advance the writer by</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public void Advance(int count) + { + if (count > RemainingSize) + { + throw new ArgumentOutOfRangeException(nameof(Remaining), "The internal buffer does not have enough buffer space"); + } + Written += count; + } + + /// <summary> + /// Resets the writer by setting the <see cref="Written"/> + /// property to 0. + /// </summary> + public void Reset() => Written = 0; + } +} diff --git a/Utils/src/Memory/ForwardOnlyMemoryReader.cs b/Utils/src/Memory/ForwardOnlyMemoryReader.cs new file mode 100644 index 0000000..c850b14 --- /dev/null +++ b/Utils/src/Memory/ForwardOnlyMemoryReader.cs @@ -0,0 +1,74 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ForwardOnlyMemoryReader.cs +* +* ForwardOnlyMemoryReader.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> + /// A mutable structure used to implement a simple foward only + /// reader for a memory segment + /// </summary> + /// <typeparam name="T">The element type</typeparam> + public struct ForwardOnlyMemoryReader<T> + { + private readonly ReadOnlyMemory<T> _segment; + private readonly int _size; + + private int _position; + + /// <summary> + /// Initializes a new <see cref="FordwardOnlyMemoryReader{T}"/> + /// of the specified type using the specified internal buffer + /// </summary> + /// <param name="buffer">The buffer to read from</param> + public ForwardOnlyMemoryReader(in ReadOnlyMemory<T> buffer) + { + _segment = buffer; + _size = buffer.Length; + _position = 0; + } + + /// <summary> + /// The remaining data window + /// </summary> + public readonly ReadOnlyMemory<T> Window => _segment[_position..]; + /// <summary> + /// The number of elements remaining in the window + /// </summary> + public readonly int WindowSize => _size - _position; + + + /// <summary> + /// Advances the window position the specified number of elements + /// </summary> + /// <param name="count">The number of elements to advance the widnow position</param> + public void Advance(int count) => _position += count; + + /// <summary> + /// Resets the sliding window to the begining of the buffer + /// </summary> + public void Reset() => _position = 0; + } +} diff --git a/Utils/src/Memory/ForwardOnlyMemoryWriter.cs b/Utils/src/Memory/ForwardOnlyMemoryWriter.cs new file mode 100644 index 0000000..4f5286d --- /dev/null +++ b/Utils/src/Memory/ForwardOnlyMemoryWriter.cs @@ -0,0 +1,122 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ForwardOnlyMemoryWriter.cs +* +* ForwardOnlyMemoryWriter.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> + /// Provides a mutable sliding buffer writer + /// </summary> + public struct ForwardOnlyMemoryWriter<T> + { + /// <summary> + /// The buffer for writing output data to + /// </summary> + public readonly Memory<T> Buffer { get; } + /// <summary> + /// The number of characters written to the buffer + /// </summary> + public int Written { readonly get; set; } + /// <summary> + /// The number of characters remaining in the buffer + /// </summary> + public readonly int RemainingSize => Buffer.Length - Written; + + /// <summary> + /// The remaining buffer window + /// </summary> + public readonly Memory<T> Remaining => Buffer[Written..]; + + /// <summary> + /// Creates a new <see cref="ForwardOnlyWriter{T}"/> assigning the specified buffer + /// </summary> + /// <param name="buffer">The buffer to write data to</param> + public ForwardOnlyMemoryWriter(in Memory<T> buffer) + { + Buffer = buffer; + Written = 0; + } + + /// <summary> + /// Returns a compiled string from the characters written to the buffer + /// </summary> + /// <returns>A string of the characters written to the buffer</returns> + public readonly override string ToString() => Buffer[..Written].ToString(); + + /// <summary> + /// Appends a sequence to the buffer + /// </summary> + /// <param name="data">The data to append to the buffer</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public void Append(ReadOnlyMemory<T> data) + { + //Make sure the current window is large enough to buffer the new string + if (data.Length > RemainingSize) + { + throw new ArgumentOutOfRangeException(nameof(Remaining), "The internal buffer does not have enough buffer space"); + } + Memory<T> window = Buffer[Written..]; + //write data to window + data.CopyTo(window); + //update char position + Written += data.Length; + } + /// <summary> + /// Appends a single item to the buffer + /// </summary> + /// <param name="c">The item to append to the buffer</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public void Append(T c) + { + //Make sure the current window is large enough to buffer the new string + if (RemainingSize == 0) + { + throw new ArgumentOutOfRangeException(nameof(Remaining), "The internal buffer does not have enough buffer space"); + } + //Write data to buffer and increment the buffer position + Buffer.Span[Written++] = c; + } + + /// <summary> + /// Advances the writer forward the specifed number of elements + /// </summary> + /// <param name="count">The number of elements to advance the writer by</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public void Advance(int count) + { + if (count > RemainingSize) + { + throw new ArgumentOutOfRangeException(nameof(count), count, "Cannot advance past the end of the buffer"); + } + Written += count; + } + + /// <summary> + /// Resets the writer by setting the <see cref="Written"/> + /// property to 0. + /// </summary> + public void Reset() => Written = 0; + } +} diff --git a/Utils/src/Memory/ForwardOnlyReader.cs b/Utils/src/Memory/ForwardOnlyReader.cs new file mode 100644 index 0000000..aa268c4 --- /dev/null +++ b/Utils/src/Memory/ForwardOnlyReader.cs @@ -0,0 +1,74 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ForwardOnlyReader.cs +* +* ForwardOnlyReader.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> + /// A mutable structure used to implement a simple foward only + /// reader for a memory segment + /// </summary> + /// <typeparam name="T">The element type</typeparam> + public ref struct ForwardOnlyReader<T> + { + private readonly ReadOnlySpan<T> _segment; + private readonly int _size; + + private int _position; + + /// <summary> + /// Initializes a new <see cref="FordwardOnlyReader{T}"/> + /// of the specified type using the specified internal buffer + /// </summary> + /// <param name="buffer">The buffer to read from</param> + public ForwardOnlyReader(in ReadOnlySpan<T> buffer) + { + _segment = buffer; + _size = buffer.Length; + _position = 0; + } + + /// <summary> + /// The remaining data window + /// </summary> + public readonly ReadOnlySpan <T> Window => _segment[_position..]; + + /// <summary> + /// The number of elements remaining in the window + /// </summary> + public readonly int WindowSize => _size - _position; + + /// <summary> + /// Advances the window position the specified number of elements + /// </summary> + /// <param name="count">The number of elements to advance the widnow position</param> + public void Advance(int count) => _position += count; + + /// <summary> + /// Resets the sliding window to the begining of the buffer + /// </summary> + public void Reset() => _position = 0; + } +} diff --git a/Utils/src/Memory/IMemoryHandle.cs b/Utils/src/Memory/IMemoryHandle.cs new file mode 100644 index 0000000..75d1cce --- /dev/null +++ b/Utils/src/Memory/IMemoryHandle.cs @@ -0,0 +1,53 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IMemoryHandle.cs +* +* IMemoryHandle.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; +using System.Buffers; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// Represents a handle for safe access to memory managed/unamanged memory + /// </summary> + /// <typeparam name="T">The type this handle represents</typeparam> + public interface IMemoryHandle<T> : IDisposable, IPinnable + { + /// <summary> + /// The size of the block as an integer + /// </summary> + /// <exception cref="OverflowException"></exception> + int IntLength { get; } + + /// <summary> + /// The number of elements in the block + /// </summary> + ulong Length { get; } + + /// <summary> + /// Gets the internal block as a span + /// </summary> + Span<T> Span { get; } + } + +} diff --git a/Utils/src/Memory/IStringSerializeable.cs b/Utils/src/Memory/IStringSerializeable.cs new file mode 100644 index 0000000..12cfe52 --- /dev/null +++ b/Utils/src/Memory/IStringSerializeable.cs @@ -0,0 +1,55 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IStringSerializeable.cs +* +* IStringSerializeable.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> + /// A interface that provides indempodent abstractions for compiling an instance + /// to its representitive string. + /// </summary> + public interface IStringSerializeable + { + /// <summary> + /// Compiles the current instance into its safe string representation + /// </summary> + /// <returns>A string of the desired representation of the current instance</returns> + string Compile(); + /// <summary> + /// Compiles the current instance into its safe string representation, and writes its + /// contents to the specified buffer writer + /// </summary> + /// <param name="writer">The ouput writer to write the serialized representation to</param> + /// <exception cref="OutOfMemoryException"></exception> + void Compile(ref ForwardOnlyWriter<char> writer); + /// <summary> + /// Compiles the current instance into its safe string representation, and writes its + /// contents to the specified buffer writer + /// </summary> + /// <param name="buffer">The buffer to write the serialized representation to</param> + /// <returns>The number of characters written to the buffer</returns> + ERRNO Compile(in Span<char> buffer); + } +} diff --git a/Utils/src/Memory/IUnmangedHeap.cs b/Utils/src/Memory/IUnmangedHeap.cs new file mode 100644 index 0000000..5d8f4bf --- /dev/null +++ b/Utils/src/Memory/IUnmangedHeap.cs @@ -0,0 +1,59 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IUnmangedHeap.cs +* +* IUnmangedHeap.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> + /// Abstraction for handling (allocating, resizing, and freeing) blocks of unmanaged memory from an unmanged heap + /// </summary> + public interface IUnmangedHeap : IDisposable + { + /// <summary> + /// Allocates a block of memory from the heap and returns a pointer to the new memory block + /// </summary> + /// <param name="size">The size (in bytes) of the element</param> + /// <param name="elements">The number of elements to allocate</param> + /// <param name="zero">An optional parameter to zero the block of memory</param> + /// <returns></returns> + IntPtr Alloc(UInt64 elements, UInt64 size, bool zero); + + /// <summary> + /// Resizes the allocated block of memory to the new size + /// </summary> + /// <param name="block">The block to resize</param> + /// <param name="elements">The new number of elements</param> + /// <param name="size">The size (in bytes) of the type</param> + /// <param name="zero">An optional parameter to zero the block of memory</param> + void Resize(ref IntPtr block, UInt64 elements, UInt64 size, bool zero); + + /// <summary> + /// Free's a previously allocated block of memory + /// </summary> + /// <param name="block">The memory to be freed</param> + /// <returns>A value indicating if the free operation succeeded</returns> + bool Free(ref IntPtr block); + } +} diff --git a/Utils/src/Memory/Memory.cs b/Utils/src/Memory/Memory.cs new file mode 100644 index 0000000..822d98c --- /dev/null +++ b/Utils/src/Memory/Memory.cs @@ -0,0 +1,456 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: Memory.cs +* +* Memory.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; +using System.IO; +using System.Buffers; +using System.Security; +using System.Threading; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// Provides optimized cross-platform maanged/umanaged safe/unsafe memory operations + /// </summary> + [SecurityCritical] + [ComVisible(false)] + public unsafe static class Memory + { + public const string SHARED_HEAP_TYPE_ENV= "VNLIB_SHARED_HEAP_TYPE"; + public const string SHARED_HEAP_INTIAL_SIZE_ENV = "VNLIB_SHARED_HEAP_SIZE"; + + /// <summary> + /// Initial shared heap size (bytes) + /// </summary> + public const ulong SHARED_HEAP_INIT_SIZE = 20971520; + + public const int MAX_BUF_SIZE = 2097152; + public const int MIN_BUF_SIZE = 16000; + + /// <summary> + /// The maximum buffer size requested by <see cref="UnsafeAlloc{T}(int, bool)"/> + /// that will use the array pool before falling back to the <see cref="Shared"/>. + /// heap. + /// </summary> + public const int MAX_UNSAFE_POOL_SIZE = 500 * 1024; + + /// <summary> + /// Provides a shared heap instance for the process to allocate memory from. + /// </summary> + /// <remarks> + /// The backing heap + /// is determined by the OS type and process environment varibles. + /// </remarks> + public static IUnmangedHeap Shared => _sharedHeap.Value; + + private static readonly Lazy<IUnmangedHeap> _sharedHeap; + + static Memory() + { + _sharedHeap = new Lazy<IUnmangedHeap>(() => InitHeapInternal(true), LazyThreadSafetyMode.PublicationOnly); + //Cleanup the heap on process exit + AppDomain.CurrentDomain.DomainUnload += DomainUnloaded; + } + + private static void DomainUnloaded(object sender, EventArgs e) + { + //Dispose the heap if allocated + if (_sharedHeap.IsValueCreated) + { + _sharedHeap.Value.Dispose(); + } + } + + /// <summary> + /// Initializes a new <see cref="IUnmangedHeap"/> determined by compilation/runtime flags + /// and operating system type for the current proccess. + /// </summary> + /// <returns>An <see cref="IUnmangedHeap"/> for the current process</returns> + /// <exception cref="SystemException"></exception> + /// <exception cref="DllNotFoundException"></exception> + public static IUnmangedHeap InitializeNewHeapForProcess() => InitHeapInternal(false); + + private static IUnmangedHeap InitHeapInternal(bool isShared) + { + bool IsWindows = OperatingSystem.IsWindows(); + //Get environment varable + string heapType = Environment.GetEnvironmentVariable(SHARED_HEAP_TYPE_ENV); + //Get inital size + string sharedSize = Environment.GetEnvironmentVariable(SHARED_HEAP_INTIAL_SIZE_ENV); + //Try to parse the shared size from the env + if (!ulong.TryParse(sharedSize, out ulong defaultSize)) + { + defaultSize = SHARED_HEAP_INIT_SIZE; + } + //Gen the private heap from its type or default + switch (heapType) + { + case "win32": + if (!IsWindows) + { + throw new PlatformNotSupportedException("Win32 private heaps are not supported on non-windows platforms"); + } + return PrivateHeap.Create(defaultSize); + case "rpmalloc": + //If the shared heap is being allocated, then return a lock free global heap + return isShared ? RpMallocPrivateHeap.GlobalHeap : new RpMallocPrivateHeap(false); + default: + return IsWindows ? PrivateHeap.Create(defaultSize) : new ProcessHeap(); + } + } + + /// <summary> + /// Gets a value that indicates if the Rpmalloc native library is loaded + /// </summary> + public static bool IsRpMallocLoaded { get; } = Environment.GetEnvironmentVariable(SHARED_HEAP_TYPE_ENV) == "rpmalloc"; + + #region Zero + /// <summary> + /// Zeros a block of memory of umanged type. If Windows is detected at runtime, calls RtlSecureZeroMemory Win32 function + /// </summary> + /// <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 + { + if (!block.IsEmpty) + { + checked + { + fixed (void* ptr = &MemoryMarshal.GetReference(block)) + { + //Calls memset + Unsafe.InitBlock(ptr, 0, (uint)(block.Length * sizeof(T))); + } + } + } + } + /// <summary> + /// Zeros a block of memory of umanged type. If Windows is detected at runtime, calls RtlSecureZeroMemory Win32 function + /// </summary> + /// <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 + { + if (!block.IsEmpty) + { + checked + { + //Pin memory and get pointer + using MemoryHandle handle = block.Pin(); + //Calls memset + Unsafe.InitBlock(handle.Pointer, 0, (uint)(block.Length * sizeof(T))); + } + } + } + + /// <summary> + /// Initializes a block of memory with zeros + /// </summary> + /// <typeparam name="T">The unmanaged</typeparam> + /// <param name="block">The block of memory to initialize</param> + public static void InitializeBlock<T>(Span<T> block) where T : unmanaged => UnsafeZeroMemory<T>(block); + /// <summary> + /// Initializes a block of memory with zeros + /// </summary> + /// <typeparam name="T">The unmanaged</typeparam> + /// <param name="block">The block of memory to initialize</param> + public static void InitializeBlock<T>(Memory<T> block) where T : unmanaged => UnsafeZeroMemory<T>(block); + + /// <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 + int size = Unsafe.SizeOf<T>(); + //Zero block + Unsafe.InitBlock(block.ToPointer(), 0, (uint)size); + } + /// <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) where T: unmanaged + { + //get thes size of the structure + int size = Unsafe.SizeOf<T>(); + //Zero block + Unsafe.InitBlock(structPtr, 0, (uint)size); + } + /// <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>(T* structPtr) where T : unmanaged + { + //get thes size of the structure + int size = Unsafe.SizeOf<T>(); + //Zero block + Unsafe.InitBlock(structPtr, 0, (uint)size); + } + + #endregion + + #region Copy + /// <summary> + /// Copies data from source memory to destination memory of an umanged data type + /// </summary> + /// <typeparam name="T">Unmanged type</typeparam> + /// <param name="source">Source data <see cref="ReadOnlySpan{T}"/></param> + /// <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, Int64 destOffset) where T : unmanaged + { + if (source.IsEmpty) + { + return; + } + if (dest.Length < (ulong)(destOffset + source.Length)) + { + throw new ArgumentException("Source data is larger than the dest data block", nameof(source)); + } + //Get long offset from the destination handle + T* offset = dest.GetOffset(destOffset); + fixed(void* src = &MemoryMarshal.GetReference(source)) + { + int byteCount = checked(source.Length * sizeof(T)); + Unsafe.CopyBlock(offset, src, (uint)byteCount); + } + } + /// <summary> + /// Copies data from source memory to destination memory of an umanged data type + /// </summary> + /// <typeparam name="T">Unmanged type</typeparam> + /// <param name="source">Source data <see cref="ReadOnlyMemory{T}"/></param> + /// <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, Int64 destOffset) where T : unmanaged + { + if (source.IsEmpty) + { + return; + } + if (dest.Length < (ulong)(destOffset + source.Length)) + { + throw new ArgumentException("Dest constraints are larger than the dest data block", nameof(source)); + } + //Get long offset from the destination handle + T* offset = dest.GetOffset(destOffset); + //Pin the source memory + using MemoryHandle srcHandle = source.Pin(); + int byteCount = checked(source.Length * sizeof(T)); + //Copy block using unsafe class + Unsafe.CopyBlock(offset, srcHandle.Pointer, (uint)byteCount); + } + /// <summary> + /// Copies data from source memory to destination memory of an umanged data type + /// </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="Span{T}"/></param> + /// <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, Int64 sourceOffset, Span<T> dest, int destOffset, int count) where T : unmanaged + { + if (count <= 0) + { + return; + } + if (source.Length < (ulong)(sourceOffset + count)) + { + throw new ArgumentException("Source constraints are larger than the source data block", nameof(count)); + } + if (dest.Length < destOffset + count) + { + throw new ArgumentOutOfRangeException(nameof(destOffset), "Destination offset range cannot exceed the size of the destination buffer"); + } + //Get offset to allow large blocks of memory + T* src = source.GetOffset(sourceOffset); + fixed(T* dst = &MemoryMarshal.GetReference(dest)) + { + //Cacl offset + T* dstoffset = dst + destOffset; + int byteCount = checked(count * sizeof(T)); + //Aligned copy + Unsafe.CopyBlock(dstoffset, src, (uint)byteCount); + } + } + /// <summary> + /// Copies data from source memory to destination memory of an umanged data type + /// </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="ArgumentOutOfRangeException"></exception> + public static void Copy<T>(MemoryHandle<T> source, Int64 sourceOffset, Memory<T> dest, int destOffset, int count) where T : unmanaged + { + if (count == 0) + { + return; + } + if (source.Length < (ulong)(sourceOffset + count)) + { + throw new ArgumentException("Source constraints are larger than the source data block", nameof(count)); + } + if(dest.Length < destOffset + count) + { + throw new ArgumentOutOfRangeException(nameof(destOffset), "Destination offset range cannot exceed the size of the destination buffer"); + } + //Get offset to allow large blocks of memory + T* src = source.GetOffset(sourceOffset); + //Pin the memory handle + using MemoryHandle handle = dest.Pin(); + //Byte count + int byteCount = checked(count * sizeof(T)); + //Dest offset + T* dst = ((T*)handle.Pointer) + destOffset; + //Aligned copy + Unsafe.CopyBlock(dst, src, (uint)byteCount); + } + #endregion + + #region Streams + /// <summary> + /// Copies data from one stream to another in specified blocks + /// </summary> + /// <param name="source">Source memory</param> + /// <param name="srcOffset">Source offset</param> + /// <param name="dest">Destination memory</param> + /// <param name="destOffst">Destination offset</param> + /// <param name="count">Number of elements to copy</param> + public static void Copy(Stream source, Int64 srcOffset, Stream dest, Int64 destOffst, Int64 count) + { + if (count == 0) + { + return; + } + if (count < 0) + { + throw new ArgumentException("Count must be a positive integer", nameof(count)); + } + //Seek streams + _ = source.Seek(srcOffset, SeekOrigin.Begin); + _ = dest.Seek(destOffst, SeekOrigin.Begin); + //Create new buffer + using IMemoryHandle<byte> buffer = Shared.Alloc<byte>(count); + Span<byte> buf = buffer.Span; + int total = 0; + do + { + //read from source + int read = source.Read(buf); + //guard + if (read == 0) + { + break; + } + //write read slice to dest + dest.Write(buf[..read]); + //update total read + total += read; + } while (total < count); + } + #endregion + + #region alloc + + /// <summary> + /// Allocates a block of unmanaged, or pooled manaaged memory depending on + /// compilation flags and runtime unamanged allocators. + /// </summary> + /// <typeparam name="T">The unamanged type to allocate</typeparam> + /// <param name="elements">The number of elements of the type within the block</param> + /// <param name="zero">Flag to zero elements during allocation before the method returns</param> + /// <returns>A handle to the block of memory</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + public static UnsafeMemoryHandle<T> UnsafeAlloc<T>(int elements, bool zero = false) where T : unmanaged + { + if (elements < 0) + { + throw new ArgumentException("Number of elements must be a positive integer", nameof(elements)); + } + if(elements > MAX_UNSAFE_POOL_SIZE || IsRpMallocLoaded) + { + // Alloc from heap + IntPtr block = Shared.Alloc((uint)elements, (uint)sizeof(T), zero); + //Init new handle + return new(Shared, block, elements); + } + else + { + return new(ArrayPool<T>.Shared, elements, zero); + } + } + + /// <summary> + /// Allocates a block of unmanaged, or pooled manaaged memory depending on + /// compilation flags and runtime unamanged allocators. + /// </summary> + /// <typeparam name="T">The unamanged type to allocate</typeparam> + /// <param name="elements">The number of elements of the type within the block</param> + /// <param name="zero">Flag to zero elements during allocation before the method returns</param> + /// <returns>A handle to the block of memory</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + public static IMemoryHandle<T> SafeAlloc<T>(int elements, bool zero = false) where T: unmanaged + { + if (elements < 0) + { + throw new ArgumentException("Number of elements must be a positive integer", nameof(elements)); + } + + //If the element count is larger than max pool size, alloc from shared heap + if (elements > MAX_UNSAFE_POOL_SIZE) + { + //Alloc from shared heap + return Shared.Alloc<T>(elements, zero); + } + else + { + //Get temp buffer from shared buffer pool + return new VnTempBuffer<T>(elements, zero); + } + } + + #endregion + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/MemoryHandle.cs b/Utils/src/Memory/MemoryHandle.cs new file mode 100644 index 0000000..a09edea --- /dev/null +++ b/Utils/src/Memory/MemoryHandle.cs @@ -0,0 +1,237 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: MemoryHandle.cs +* +* MemoryHandle.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; +using System.Buffers; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +using Microsoft.Win32.SafeHandles; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// Provides a wrapper for using umanged memory handles from an assigned <see cref="PrivateHeap"/> for <see cref="UnmanagedType"/> types + /// </summary> + /// <remarks> + /// 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 + { + /// <summary> + /// New <typeparamref name="T"/>* pointing to the base of the allocated block + /// </summary> + /// <exception cref="ObjectDisposedException"></exception> + public unsafe T* Base + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetOffset(0); + } + /// <summary> + /// New <see cref="IntPtr"/> pointing to the base of the allocated block + /// </summary> + /// <exception cref="ObjectDisposedException"></exception> + public unsafe IntPtr BasePtr + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (IntPtr)GetOffset(0); + } + /// <summary> + /// Gets a span over the entire allocated block + /// </summary> + /// <returns>A <see cref="Span{T}"/> over the internal data</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="OverflowException"></exception> + public unsafe Span<T> Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + this.ThrowIfClosed(); + return _length == 0 ? Span<T>.Empty : new Span<T>(Base, IntLength); + } + } + + private readonly bool ZeroMemory; + private readonly IUnmangedHeap Heap; + private ulong _length; + + /// <summary> + /// Number of elements allocated to the current instance + /// </summary> + public ulong Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _length; + } + /// <summary> + /// Number of elements in the memory block casted to an integer + /// </summary> + /// <exception cref="OverflowException"></exception> + public int IntLength + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => checked((int)_length); + } + + /// <summary> + /// Number of bytes allocated to the current instance + /// </summary> + /// <exception cref="OverflowException"></exception> + public unsafe ulong ByteLength + { + //Check for overflows when converting to bytes (should run out of memory before this is an issue, but just incase) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => checked(_length * (UInt64)sizeof(T)); + } + + /// <summary> + /// Creates a new memory handle, for which is holds ownership, and allocates the number of elements specified on the heap. + /// </summary> + /// <param name="heap">The heap to allocate/deallocate memory from</param> + /// <param name="elements">Number of elements to allocate</param> + /// <param name="zero">Zero all memory during allocations from heap</param> + /// <param name="initial">The initial block of allocated memory to wrap</param> + internal MemoryHandle(IUnmangedHeap heap, IntPtr initial, ulong elements, bool zero) : base(true) + { + //Set element size (always allocate at least 1 object) + _length = elements; + ZeroMemory = zero; + //assign heap ref + Heap = heap; + handle = initial; + } + + /// <summary> + /// Resizes the current handle on the heap + /// </summary> + /// <param name="elements">Positive number of elemnts the current handle should referrence</param> + /// <exception cref="OverflowException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public unsafe void Resize(ulong elements) + { + this.ThrowIfClosed(); + //Update size (should never be less than inital size) + _length = elements; + //Re-alloc (Zero if required) + try + { + Heap.Resize(ref handle, Length, (ulong)sizeof(T), ZeroMemory); + } + //Catch the disposed exception so we can invalidate the current ptr + catch (ObjectDisposedException) + { + base.handle = IntPtr.Zero; + //Set as invalid so release does not get called + base.SetHandleAsInvalid(); + //Propagate the exception + throw; + } + } + /// <summary> + /// Gets an offset pointer from the base postion to the number of bytes specified. Performs bounds checks + /// </summary> + /// <param name="elements">Number of elements of type to offset</param> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <returns><typeparamref name="T"/> pointer to the memory offset specified</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe T* GetOffset(ulong elements) + { + if (elements >= _length) + { + throw new ArgumentOutOfRangeException(nameof(elements), "Element offset cannot be larger than allocated size"); + } + this.ThrowIfClosed(); + //Get ptr and offset it + T* bs = ((T*)handle) + elements; + return bs; + } + + ///<inheritdoc/> + ///<exception cref="ObjectDisposedException"></exception> + ///<exception cref="ArgumentOutOfRangeException"></exception> + public unsafe MemoryHandle Pin(int elementIndex) + { + //Get ptr and guard checks before adding the referrence + T* ptr = GetOffset((ulong)elementIndex); + + bool addRef = false; + //use the pinned field as success val + DangerousAddRef(ref addRef); + //Create a new system.buffers memory handle from the offset ptr address + return !addRef + ? 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() + { + //Dec count on release + DangerousRelease(); + } + + + ///<inheritdoc/> + protected override bool ReleaseHandle() + { + //Return result of free + return Heap.Free(ref handle); + } + + + + /// <summary> + /// Determines if the memory blocks are equal by comparing their base addresses. + /// </summary> + /// <param name="other"><see cref="MemoryHandle{T}"/> to compare</param> + /// <returns>true if the block of memory is the same, false if the handle's size does not + /// match or the base addresses do not match even if they point to an overlapping address space</returns> + /// <exception cref="ObjectDisposedException"></exception> + public bool Equals(MemoryHandle<T> other) + { + this.ThrowIfClosed(); + other.ThrowIfClosed(); + return _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) + { + //If the handle is invalid or closed return an empty span + return handle.IsClosed || handle.IsInvalid || handle._length == 0 ? Span<T>.Empty : handle.Span; + } + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/PrivateBuffersMemoryPool.cs b/Utils/src/Memory/PrivateBuffersMemoryPool.cs new file mode 100644 index 0000000..1e85207 --- /dev/null +++ b/Utils/src/Memory/PrivateBuffersMemoryPool.cs @@ -0,0 +1,67 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: PrivateBuffersMemoryPool.cs +* +* PrivateBuffersMemoryPool.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; +using System.Buffers; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// Provides a <see cref="MemoryPool{T}"/> wrapper for using unmanged <see cref="PrivateHeap"/>s + /// </summary> + /// <typeparam name="T">Unamanged memory type to provide data memory instances from</typeparam> + public sealed class PrivateBuffersMemoryPool<T> : MemoryPool<T> where T : unmanaged + { + private readonly IUnmangedHeap Heap; + + internal PrivateBuffersMemoryPool(IUnmangedHeap heap):base() + { + this.Heap = heap; + } + ///<inheritdoc/> + public override int MaxBufferSize => int.MaxValue; + ///<inheritdoc/> + ///<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); + + /// <summary> + /// Allocates a new <see cref="MemoryManager{T}"/> of a different data type from the pool + /// </summary> + /// <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 + { + return new SysBufferMemoryManager<TDifType>(Heap, (uint)minBufferSize, false); + } + ///<inheritdoc/> + protected override void Dispose(bool disposing) + { + //Dispose the heap + Heap.Dispose(); + } + } +} diff --git a/Utils/src/Memory/PrivateHeap.cs b/Utils/src/Memory/PrivateHeap.cs new file mode 100644 index 0000000..5d97506 --- /dev/null +++ b/Utils/src/Memory/PrivateHeap.cs @@ -0,0 +1,184 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: PrivateHeap.cs +* +* PrivateHeap.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; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Runtime.InteropServices; + +using DWORD = System.Int64; +using SIZE_T = System.UInt64; +using LPVOID = System.IntPtr; + +namespace VNLib.Utils.Memory +{ + ///<summary> + ///<para> + /// Provides a win32 private heap managed wrapper class + ///</para> + ///</summary> + ///<remarks> + /// <see cref="PrivateHeap"/> implements <see cref="SafeHandle"/> and tracks allocated blocks by its + /// referrence counter. Allocations increment the count, and free's decrement the count, so the heap may + /// be disposed safely + /// </remarks> + [ComVisible(false)] + [SupportedOSPlatform("Windows")] + public sealed class PrivateHeap : UnmanagedHeapBase + { + private const string KERNEL_DLL = "Kernel32"; + + #region Extern + //Heap flags + public const DWORD HEAP_NO_FLAGS = 0x00; + public const DWORD HEAP_GENERATE_EXCEPTIONS = 0x04; + public const DWORD HEAP_NO_SERIALIZE = 0x01; + public const DWORD HEAP_REALLOC_IN_PLACE_ONLY = 0x10; + public const DWORD HEAP_ZERO_MEMORY = 0x08; + + [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern LPVOID HeapAlloc(IntPtr hHeap, DWORD flags, SIZE_T dwBytes); + [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern LPVOID HeapReAlloc(IntPtr hHeap, DWORD dwFlags, LPVOID lpMem, SIZE_T dwBytes); + [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool HeapFree(IntPtr hHeap, DWORD dwFlags, LPVOID lpMem); + + [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern LPVOID HeapCreate(DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize); + [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool HeapDestroy(IntPtr hHeap); + [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern bool HeapValidate(IntPtr hHeap, DWORD dwFlags, LPVOID lpMem); + [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.U8)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern SIZE_T HeapSize(IntPtr hHeap, DWORD flags, LPVOID lpMem); + + #endregion + + /// <summary> + /// Create a new <see cref="PrivateHeap"/> with the specified sizes and flags + /// </summary> + /// <param name="initialSize">Intial size of the heap</param> + /// <param name="maxHeapSize">Maximum size allowed for the heap (disabled = 0, default)</param> + /// <param name="flags">Defalt heap flags to set globally for all blocks allocated by the heap (default = 0)</param> + public static PrivateHeap Create(SIZE_T initialSize, SIZE_T maxHeapSize = 0, DWORD flags = HEAP_NO_FLAGS) + { + //Call create, throw exception if the heap falled to allocate + IntPtr heapHandle = HeapCreate(flags, initialSize, maxHeapSize); + if (heapHandle == IntPtr.Zero) + { + throw new NativeMemoryException("Heap could not be created"); + } +#if TRACE + Trace.WriteLine($"Win32 private heap {heapHandle:x} created"); +#endif + //Heap has been created so we can wrap it + return new(heapHandle); + } + /// <summary> + /// LIFETIME WARNING. Consumes a valid win32 handle and will manage it's lifetime once constructed. + /// Locking and memory blocks will attempt to be allocated from this heap handle. + /// </summary> + /// <param name="win32HeapHandle">An open and valid handle to a win32 private heap</param> + /// <returns>A wrapper around the specified heap</returns> + public static PrivateHeap ConsumeExisting(IntPtr win32HeapHandle) => new (win32HeapHandle); + + private PrivateHeap(IntPtr heapPtr) : base(false, true) => handle = heapPtr; + + /// <summary> + /// Retrieves the size of a memory block allocated from the current heap. + /// </summary> + /// <param name="block">The pointer to a block of memory to get the size of</param> + /// <returns>The size of the block of memory, (SIZE_T)-1 if the operation fails</returns> + public SIZE_T HeapSize(ref LPVOID block) => HeapSize(handle, HEAP_NO_FLAGS, block); + + /// <summary> + /// Validates the specified block of memory within the current heap instance. This function will block hte + /// </summary> + /// <param name="block">Pointer to the block of memory to validate</param> + /// <returns>True if the block is valid, false otherwise</returns> + public bool Validate(ref LPVOID block) + { + bool result; + //Lock the heap before validating + HeapLock.Wait(); + //validate the block on the current heap + result = HeapValidate(handle, HEAP_NO_FLAGS, block); + //Unlock the heap + HeapLock.Release(); + return result; + + } + /// <summary> + /// Validates the current heap instance. The function scans all the memory blocks in the heap and verifies that the heap control structures maintained by + /// the heap manager are in a consistent state. + /// </summary> + /// <returns>If the specified heap or memory block is valid, the return value is nonzero.</returns> + /// <remarks>This can be a consuming operation which will block all allocations</remarks> + public bool Validate() + { + bool result; + //Lock the heap before validating + HeapLock.Wait(); + //validate the entire heap + result = HeapValidate(handle, HEAP_NO_FLAGS, IntPtr.Zero); + //Unlock the heap + HeapLock.Release(); + return result; + } + + ///<inheritdoc/> + protected override bool ReleaseHandle() + { +#if TRACE + Trace.WriteLine($"Win32 private heap {handle:x} destroyed"); +#endif + return HeapDestroy(handle) && base.ReleaseHandle(); + } + ///<inheritdoc/> + protected override sealed LPVOID AllocBlock(ulong elements, ulong size, bool zero) + { + ulong bytes = checked(elements * size); + return HeapAlloc(handle, zero ? HEAP_ZERO_MEMORY : HEAP_NO_FLAGS, bytes); + } + ///<inheritdoc/> + protected override sealed bool FreeBlock(LPVOID block) => HeapFree(handle, HEAP_NO_FLAGS, block); + ///<inheritdoc/> + protected override sealed LPVOID ReAllocBlock(LPVOID block, ulong elements, ulong size, bool zero) + { + ulong bytes = checked(elements * size); + return HeapReAlloc(handle, zero ? HEAP_ZERO_MEMORY : HEAP_NO_FLAGS, block, bytes); + } + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/PrivateString.cs b/Utils/src/Memory/PrivateString.cs new file mode 100644 index 0000000..cd3b1f6 --- /dev/null +++ b/Utils/src/Memory/PrivateString.cs @@ -0,0 +1,185 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: PrivateString.cs +* +* PrivateString.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; +using System.Diagnostics.CodeAnalysis; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// Provides a wrapper class that will have unsafe access to the memory of + /// the specified <see cref="string"/> provided during object creation. + /// </summary> + /// <remarks>The value of the memory the protected string points to is undefined when the instance is disposed</remarks> + public class PrivateString : PrivateStringManager, IEquatable<PrivateString>, IEquatable<string>, ICloneable + { + protected string StrRef => base[0]!; + private readonly bool OwnsReferrence; + + /// <summary> + /// Creates a new <see cref="PrivateString"/> over the specified string and the memory it points to. + /// </summary> + /// <param name="data">The <see cref="string"/> instance pointing to the memory to protect</param> + /// <param name="ownsReferrence">Does the current instance "own" the memory the data parameter points to</param> + /// <remarks>You should no longer reference the input string directly</remarks> + public PrivateString(string data, bool ownsReferrence = true) : base(1) + { + //Create a private string manager to store referrence to string + base[0] = data ?? throw new ArgumentNullException(nameof(data)); + OwnsReferrence = ownsReferrence; + } + + //Create private string from a string + public static explicit operator PrivateString?(string? data) + { + //Allow passing null strings during implicit casting + return data == null ? null : new(data); + } + + public static PrivateString? ToPrivateString(string? value) + { + return value == null ? null : new PrivateString(value, true); + } + + //Cast to string + public static explicit operator string (PrivateString str) + { + //Check if disposed, or return the string + str.Check(); + return str.StrRef; + } + + public static implicit operator ReadOnlySpan<char>(PrivateString str) + { + return str.Disposed ? Span<char>.Empty : str.StrRef.AsSpan(); + } + + /// <summary> + /// Gets the value of the internal string as a <see cref="ReadOnlySpan{T}"/> + /// </summary> + /// <returns>The <see cref="ReadOnlySpan{T}"/> referrence to the internal string</returns> + /// <exception cref="ObjectDisposedException"></exception> + public ReadOnlySpan<char> ToReadOnlySpan() + { + Check(); + return StrRef.AsSpan(); + } + + ///<inheritdoc/> + public bool Equals(string? other) + { + Check(); + return StrRef.Equals(other); + } + ///<inheritdoc/> + public bool Equals(PrivateString? other) + { + Check(); + return other != null && StrRef.Equals(other.StrRef); + } + ///<inheritdoc/> + public override bool Equals(object? other) + { + Check(); + return other is PrivateString otherRef && StrRef.Equals(otherRef); + } + ///<inheritdoc/> + public bool Equals(ReadOnlySpan<char> other) + { + Check(); + return StrRef.AsSpan().SequenceEqual(other); + } + /// <summary> + /// Creates a deep copy of the internal string and returns that copy + /// </summary> + /// <returns>A deep copy of the internal string</returns> + public override string ToString() + { + Check(); + return new(StrRef.AsSpan()); + } + /// <summary> + /// String length + /// </summary> + /// <exception cref="ObjectDisposedException"></exception> + public int Length + { + get + { + Check(); + return StrRef.Length; + } + } + /// <summary> + /// Indicates whether the underlying string is null or an empty string ("") + /// </summary> + /// <param name="ps"></param> + /// <returns>True if the parameter is null, or an empty string (""). False otherwise</returns> + public static bool IsNullOrEmpty([NotNullWhen(false)] PrivateString? ps) => ps == null|| ps.Length == 0; + + /// <summary> + /// The hashcode of the underlying string + /// </summary> + /// <returns></returns> + public override int GetHashCode() + { + Check(); + return StrRef.GetHashCode(); + } + + /// <summary> + /// Creates a new deep copy of the current instance that is an independent <see cref="PrivateString"/> + /// </summary> + /// <returns>The new <see cref="PrivateString"/> instance</returns> + /// <exception cref="ObjectDisposedException"></exception> + public override object Clone() + { + Check(); + //Copy all contents of string to another reference + string clone = new (StrRef.AsSpan()); + //return a new private string + return new PrivateString(clone, true); + } + + ///<inheritdoc/> + protected override void Free() + { + Erase(); + } + + /// <summary> + /// Erases the contents of the internal CLR string + /// </summary> + public void Erase() + { + //Only dispose the instance if we own the memory + if (OwnsReferrence && !Disposed) + { + base.Free(); + } + } + + + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/PrivateStringManager.cs b/Utils/src/Memory/PrivateStringManager.cs new file mode 100644 index 0000000..9ed8f5f --- /dev/null +++ b/Utils/src/Memory/PrivateStringManager.cs @@ -0,0 +1,117 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: PrivateStringManager.cs +* +* PrivateStringManager.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> + /// When inherited by a class, provides a safe string storage that zeros a CLR string memory on disposal + /// </summary> + public class PrivateStringManager : VnDisposeable, ICloneable + { + /// <summary> + /// Strings to be cleared when exiting + /// </summary> + private readonly string?[] ProtectedElements; + /// <summary> + /// Gets or sets a string referrence into the protected elements store + /// </summary> + /// <param name="index"></param> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <returns>Referrence to string associated with the index</returns> + protected string? this[int index] + { + get + { + Check(); + return ProtectedElements[index]; + } + set + { + Check(); + //Check to see if the string has been interned + if (!string.IsNullOrEmpty(value) && string.IsInterned(value) != null) + { + throw new ArgumentException($"The specified string has been CLR interned and cannot be stored in {nameof(PrivateStringManager)}"); + } + //Clear the old value before setting the new one + if (!string.IsNullOrEmpty(ProtectedElements[index])) + { + Memory.UnsafeZeroMemory<char>(ProtectedElements[index]); + } + //set new value + ProtectedElements[index] = value; + } + } + /// <summary> + /// Create a new instance with fixed array size + /// </summary> + /// <param name="elements">Number of elements to protect</param> + public PrivateStringManager(int elements) + { + //Allocate the string array + ProtectedElements = new string[elements]; + } + ///<inheritdoc/> + protected override void Free() + { + //Zero all strings specified + for (int i = 0; i < ProtectedElements.Length; i++) + { + if (!string.IsNullOrEmpty(ProtectedElements[i])) + { + //Zero the string memory + Memory.UnsafeZeroMemory<char>(ProtectedElements[i]); + //Set to null + ProtectedElements[i] = null; + } + } + } + + /// <summary> + /// Creates a deep copy for a new independent <see cref="PrivateStringManager"/> + /// </summary> + /// <returns>A new independent <see cref="PrivateStringManager"/> instance</returns> + /// <remarks>Be careful duplicating large instances, and make sure clones are properly disposed if necessary</remarks> + /// <exception cref="ObjectDisposedException"></exception> + public virtual object Clone() + { + Check(); + PrivateStringManager other = new (ProtectedElements.Length); + //Copy all strings to the other instance + for(int i = 0; i < ProtectedElements.Length; i++) + { + //Copy all strings and store their copies in the new array + other.ProtectedElements[i] = this.ProtectedElements[i].AsSpan().ToString(); + } + //return the new copy + return other; + } + } +} diff --git a/Utils/src/Memory/ProcessHeap.cs b/Utils/src/Memory/ProcessHeap.cs new file mode 100644 index 0000000..4f06d52 --- /dev/null +++ b/Utils/src/Memory/ProcessHeap.cs @@ -0,0 +1,82 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ProcessHeap.cs +* +* ProcessHeap.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; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// Provides a <see cref="IUnmangedHeap"/> wrapper for the <see cref="Marshal"/> virtualalloc + /// global heap methods + /// </summary> + [ComVisible(false)] + public unsafe class ProcessHeap : VnDisposeable, IUnmangedHeap + { + /// <summary> + /// Initalizes a new global (cross platform) process heap + /// </summary> + public ProcessHeap() + { +#if TRACE + Trace.WriteLine($"Default heap instnace created {GetHashCode():x}"); +#endif + } + + ///<inheritdoc/> + ///<exception cref="OverflowException"></exception> + ///<exception cref="OutOfMemoryException"></exception> + public IntPtr Alloc(ulong elements, ulong size, bool zero) + { + return zero + ? (IntPtr)NativeMemory.AllocZeroed((nuint)elements, (nuint)size) + : (IntPtr)NativeMemory.Alloc((nuint)elements, (nuint)size); + } + ///<inheritdoc/> + public bool Free(ref IntPtr block) + { + //Free native mem from ptr + NativeMemory.Free(block.ToPointer()); + block = IntPtr.Zero; + return true; + } + ///<inheritdoc/> + protected override void Free() + { +#if TRACE + Trace.WriteLine($"Default heap instnace disposed {GetHashCode():x}"); +#endif + } + ///<inheritdoc/> + ///<exception cref="OverflowException"></exception> + ///<exception cref="OutOfMemoryException"></exception> + public void Resize(ref IntPtr block, ulong elements, ulong size, bool zero) + { + nuint bytes = checked((nuint)(elements * size)); + IntPtr old = block; + block = (IntPtr)NativeMemory.Realloc(old.ToPointer(), bytes); + } + } +} diff --git a/Utils/src/Memory/RpMallocPrivateHeap.cs b/Utils/src/Memory/RpMallocPrivateHeap.cs new file mode 100644 index 0000000..70c8a7f --- /dev/null +++ b/Utils/src/Memory/RpMallocPrivateHeap.cs @@ -0,0 +1,279 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: RpMallocPrivateHeap.cs +* +* RpMallocPrivateHeap.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; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +using size_t = System.UInt64; +using LPVOID = System.IntPtr; +using LPHEAPHANDLE = System.IntPtr; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// A wrapper class for cross platform RpMalloc implementation. + /// </summary> + [ComVisible(false)] + public sealed class RpMallocPrivateHeap : UnmanagedHeapBase + { + const string DLL_NAME = "rpmalloc"; + + #region statics + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern int rpmalloc_initialize(); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern void rpmalloc_finalize(); + + //Heap api + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern LPHEAPHANDLE rpmalloc_heap_acquire(); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern void rpmalloc_heap_release(LPHEAPHANDLE heap); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern LPVOID rpmalloc_heap_alloc(LPHEAPHANDLE heap, size_t size); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern LPVOID rpmalloc_heap_aligned_alloc(LPHEAPHANDLE heap, size_t alignment, size_t size); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern LPVOID rpmalloc_heap_calloc(LPHEAPHANDLE heap, size_t num, size_t size); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern LPVOID rpmalloc_heap_aligned_calloc(LPHEAPHANDLE heap, size_t alignment, size_t num, size_t size); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern LPVOID rpmalloc_heap_realloc(LPHEAPHANDLE heap, LPVOID ptr, size_t size, nuint flags); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern LPVOID rpmalloc_heap_aligned_realloc(LPHEAPHANDLE heap, LPVOID ptr, size_t alignment, size_t size, nuint flags); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern void rpmalloc_heap_free(LPHEAPHANDLE heap, LPVOID ptr); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern void rpmalloc_heap_free_all(LPHEAPHANDLE heap); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern void rpmalloc_heap_thread_set_current(LPHEAPHANDLE heap); + + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern void rpmalloc_thread_initialize(); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern int rpmalloc_is_thread_initialized(); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern void rpmalloc_thread_finalize(int release_caches); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern LPVOID rpmalloc(size_t size); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern LPVOID rpcalloc(size_t num, size_t size); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern LPVOID rprealloc(LPVOID ptr, size_t size); + [DllImport(DLL_NAME, ExactSpelling = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + static extern void rpfree(LPVOID ptr); + + #endregion + + private class RpMallocGlobalHeap : IUnmangedHeap + { + IntPtr IUnmangedHeap.Alloc(ulong elements, ulong size, bool zero) + { + return RpMalloc(elements, (nuint)size, zero); + } + + //Global heap does not need to be disposed + void IDisposable.Dispose() + { } + + bool IUnmangedHeap.Free(ref IntPtr block) + { + //Free the block + RpFree(ref block); + return true; + } + + void IUnmangedHeap.Resize(ref IntPtr block, ulong elements, ulong size, bool zero) + { + //Try to resize the block + IntPtr resize = RpRealloc(block, elements, (nuint)size); + + if (resize == IntPtr.Zero) + { + throw new NativeMemoryOutOfMemoryException("Failed to resize the block"); + } + //assign ptr + block = resize; + } + } + + /// <summary> + /// <para> + /// A <see cref="IUnmangedHeap"/> API for the RPMalloc library if loaded. + /// </para> + /// <para> + /// This heap is thread safe and may be converted to a <see cref="MemoryManager{T}"/> + /// infinitley and disposed safely. + /// </para> + /// <para> + /// If the native library is not loaded, calls to this API will throw a <see cref="DllNotFoundException"/>. + /// </para> + /// </summary> + public static IUnmangedHeap GlobalHeap { get; } = new RpMallocGlobalHeap(); + + /// <summary> + /// <para> + /// Initializes RpMalloc for the current thread and alloctes a block of memory + /// </para> + /// </summary> + /// <param name="elements">The number of elements to allocate</param> + /// <param name="size">The number of bytes per element type (aligment)</param> + /// <param name="zero">Zero the block of memory before returning</param> + /// <returns>A pointer to the block, (zero if failed)</returns> + public static LPVOID RpMalloc(size_t elements, nuint size, bool zero) + { + //See if the current thread has been initialized + if (rpmalloc_is_thread_initialized() == 0) + { + //Initialize the current thread + rpmalloc_thread_initialize(); + } + //Alloc block + LPVOID block; + if (zero) + { + block = rpcalloc(elements, size); + } + else + { + //Calculate the block size + ulong blockSize = checked(elements * size); + block = rpmalloc(blockSize); + } + return block; + } + + /// <summary> + /// Frees a block of memory allocated by RpMalloc + /// </summary> + /// <param name="block">A ref to the pointer of the block to free</param> + public static void RpFree(ref LPVOID block) + { + if (block != IntPtr.Zero) + { + rpfree(block); + block = IntPtr.Zero; + } + } + + /// <summary> + /// Attempts to re-allocate the specified block on the global heap + /// </summary> + /// <param name="block">A pointer to a previously allocated block of memory</param> + /// <param name="elements">The number of elements in the block</param> + /// <param name="size">The number of bytes in the element</param> + /// <returns>A pointer to the new block if the reallocation succeeded, null if the resize failed</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="OverflowException"></exception> + public static LPVOID RpRealloc(LPVOID block, size_t elements, nuint size) + { + if(block == IntPtr.Zero) + { + throw new ArgumentException("The supplied block is not valid", nameof(block)); + } + //Calc new block size + size_t blockSize = checked(elements * size); + return rprealloc(block, blockSize); + } + + #region instance + + /// <summary> + /// Initializes a new RpMalloc first class heap to allocate memory blocks from + /// </summary> + /// <param name="zeroAll">A global flag to zero all blocks of memory allocated</param> + /// <exception cref="NativeMemoryOutOfMemoryException"></exception> + public RpMallocPrivateHeap(bool zeroAll):base(zeroAll, true) + { + //Alloc the heap + handle = rpmalloc_heap_acquire(); + if(IsInvalid) + { + throw new NativeMemoryException("Failed to aquire a new heap"); + } +#if TRACE + Trace.WriteLine($"RPMalloc heap {handle:x} created"); +#endif + } + ///<inheritdoc/> + protected override bool ReleaseHandle() + { +#if TRACE + Trace.WriteLine($"RPMalloc heap {handle:x} destroyed"); +#endif + //Release all heap memory + rpmalloc_heap_free_all(handle); + //Destroy the heap + rpmalloc_heap_release(handle); + //Release base + return base.ReleaseHandle(); + } + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected sealed override LPVOID AllocBlock(ulong elements, ulong size, bool zero) + { + //Alloc or calloc and initalize + return zero ? rpmalloc_heap_calloc(handle, elements, size) : rpmalloc_heap_alloc(handle, checked(size * elements)); + } + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected sealed override bool FreeBlock(LPVOID block) + { + //Free block + rpmalloc_heap_free(handle, block); + return true; + } + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected sealed override LPVOID ReAllocBlock(LPVOID block, ulong elements, ulong size, bool zero) + { + //Realloc + return rpmalloc_heap_realloc(handle, block, checked(elements * size), 0); + } + #endregion + } +} diff --git a/Utils/src/Memory/SubSequence.cs b/Utils/src/Memory/SubSequence.cs new file mode 100644 index 0000000..3800fb5 --- /dev/null +++ b/Utils/src/Memory/SubSequence.cs @@ -0,0 +1,113 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: SubSequence.cs +* +* SubSequence.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; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// 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 struct SubSequence<T> : IEquatable<SubSequence<T>> where T: unmanaged + { + private readonly MemoryHandle<T> _handle; + /// <summary> + /// The number of elements in the current window + /// </summary> + public readonly int Size { get; } + + /// <summary> + /// Creates a new <see cref="SubSequence{T}"/> to the handle to get a window of the block + /// </summary> + /// <param name="block"></param> + /// <param name="offset"></param> + /// <param name="size"></param> +#if TARGET_64_BIT + public SubSequence(MemoryHandle<T> block, ulong offset, int size) +#else + public SubSequence(MemoryHandle<T> block, int offset, int size) +#endif + { + _offset = offset; + Size = size >= 0 ? size : throw new ArgumentOutOfRangeException(nameof(size)); + _handle = block ?? throw new ArgumentNullException(nameof(block)); + } + + +#if TARGET_64_BIT + private readonly ulong _offset; +#else + private readonly int _offset; +#endif + /// <summary> + /// Gets a <see cref="Span{T}"/> that is offset from the base of the handle + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"></exception> + +#if TARGET_64_BIT + public readonly Span<T> Span => Size > 0 ? _handle.GetOffsetSpan(_offset, Size) : Span<T>.Empty; +#else + public readonly Span<T> Span => Size > 0 ? _handle.Span.Slice(_offset, Size) : Span<T>.Empty; +#endif + + /// <summary> + /// Slices the current sequence into a smaller <see cref="SubSequence{T}"/> + /// </summary> + /// <param name="offset">The relative offset from the current window offset</param> + /// <param name="size">The size of the block</param> + /// <returns>A <see cref="SubSequence{T}"/> of the current sequence</returns> + public readonly SubSequence<T> Slice(uint offset, int size) => new (_handle, _offset + checked((int)offset), size); + + /// <summary> + /// Returns the signed 32-bit hashcode + /// </summary> + /// <returns>A signed 32-bit integer that represents the hashcode for the current instance</returns> + /// <exception cref="ObjectDisposedException"></exception> + public readonly override int GetHashCode() => _handle.GetHashCode() + _offset.GetHashCode(); + + ///<inheritdoc/> + public readonly bool Equals(SubSequence<T> other) => Span.SequenceEqual(other.Span); + + ///<inheritdoc/> + public readonly override bool Equals(object? obj) => obj is SubSequence<T> other && Equals(other); + + /// <summary> + /// Determines if two <see cref="SubSequence{T}"/> are equal + /// </summary> + /// <param name="left"></param> + /// <param name="right"></param> + /// <returns>True if the sequences are equal, false otherwise</returns> + public static bool operator ==(SubSequence<T> left, SubSequence<T> right) => left.Equals(right); + /// <summary> + /// Determines if two <see cref="SubSequence{T}"/> are not equal + /// </summary> + /// <param name="left"></param> + /// <param name="right"></param> + /// <returns>True if the sequences are not equal, false otherwise</returns> + public static bool operator !=(SubSequence<T> left, SubSequence<T> right) => !left.Equals(right); + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/SysBufferMemoryManager.cs b/Utils/src/Memory/SysBufferMemoryManager.cs new file mode 100644 index 0000000..040467f --- /dev/null +++ b/Utils/src/Memory/SysBufferMemoryManager.cs @@ -0,0 +1,102 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: SysBufferMemoryManager.cs +* +* SysBufferMemoryManager.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; +using System.Buffers; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// Provides an unmanaged System.Buffers integration with zero-cost pinning. Uses <see cref="MemoryHandle{T}"/> + /// 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 + { + private readonly IMemoryHandle<T> BackingMemory; + private readonly bool _ownsHandle; + + /// <summary> + /// Consumes an exisitng <see cref="MemoryHandle{T}"/> to provide <see cref="Memory"/> wrappers. + /// The handle should no longer be referrenced directly + /// </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) + { + BackingMemory = existingHandle; + _ownsHandle = ownsHandle; + } + + /// <summary> + /// Allocates a fized size buffer from the specified unmanaged <see cref="PrivateHeap"/> + /// </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, ulong elements, bool zero) + { + BackingMemory = heap.Alloc<T>(elements, zero); + _ownsHandle = true; + } + + ///<inheritdoc/> + protected override bool TryGetArray(out ArraySegment<T> segment) + { + //Always false since no array is available + segment = default; + return false; + } + + ///<inheritdoc/> + ///<exception cref="OverflowException"></exception> + ///<exception cref="ObjectDisposedException"></exception> + public override Span<T> GetSpan() => BackingMemory.Span; + + /// <summary> + /// <inheritdoc/> + /// </summary> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public unsafe override MemoryHandle Pin(int elementIndex = 0) + { + return BackingMemory.Pin(elementIndex); + } + + ///<inheritdoc/> + public override void Unpin() + {} + + ///<inheritdoc/> + protected override void Dispose(bool disposing) + { + if (_ownsHandle) + { + BackingMemory.Dispose(); + } + } + } +} diff --git a/Utils/src/Memory/UnmanagedHeapBase.cs b/Utils/src/Memory/UnmanagedHeapBase.cs new file mode 100644 index 0000000..5c92aff --- /dev/null +++ b/Utils/src/Memory/UnmanagedHeapBase.cs @@ -0,0 +1,185 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: UnmanagedHeapBase.cs +* +* UnmanagedHeapBase.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; +using System.Threading; +using System.Runtime.InteropServices; + +using Microsoft.Win32.SafeHandles; + +using size_t = System.UInt64; +using LPVOID = System.IntPtr; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// Provides a synchronized base methods for accessing unmanaged memory. Implements <see cref="SafeHandle"/> + /// for safe disposal of heaps + /// </summary> + public abstract class UnmanagedHeapBase : SafeHandleZeroOrMinusOneIsInvalid, IUnmangedHeap + { + /// <summary> + /// The heap synchronization handle + /// </summary> + protected readonly SemaphoreSlim HeapLock; + /// <summary> + /// The global heap zero flag + /// </summary> + protected readonly bool GlobalZero; + + /// <summary> + /// Initalizes the unmanaged heap base class (init synchronization handle) + /// </summary> + /// <param name="globalZero">A global flag to zero all blocks of memory during allocation</param> + /// <param name="ownsHandle">A flag that indicates if the handle is owned by the instance</param> + protected UnmanagedHeapBase(bool globalZero, bool ownsHandle) : base(ownsHandle) + { + HeapLock = new(1, 1); + GlobalZero = globalZero; + } + + ///<inheritdoc/> + ///<remarks>Increments the handle count</remarks> + ///<exception cref="OutOfMemoryException"></exception> + ///<exception cref="ObjectDisposedException"></exception> + public LPVOID Alloc(size_t elements, size_t size, bool zero) + { + //Force zero if global flag is set + zero |= GlobalZero; + bool handleCountIncremented = false; + //Increment handle count to prevent premature release + DangerousAddRef(ref handleCountIncremented); + //Failed to increment ref count, class has been disposed + if (!handleCountIncremented) + { + throw new ObjectDisposedException("The handle has been released"); + } + try + { + //wait for lock + HeapLock.Wait(); + //Alloc block + LPVOID block = AllocBlock(elements, size, zero); + //release lock + HeapLock.Release(); + //Check if block was allocated + return block != IntPtr.Zero ? block : throw new NativeMemoryOutOfMemoryException("Failed to allocate the requested block"); + } + catch + { + //Decrement handle count since allocation failed + DangerousRelease(); + throw; + } + } + ///<inheritdoc/> + ///<remarks>Decrements the handle count</remarks> + public bool Free(ref LPVOID block) + { + bool result; + //If disposed, set the block handle to zero and exit to avoid raising exceptions during finalization + if (IsClosed || IsInvalid) + { + block = IntPtr.Zero; + return true; + } + //wait for lock + HeapLock.Wait(); + //Free block + result = FreeBlock(block); + //Release lock before releasing handle + HeapLock.Release(); + //Decrement handle count + DangerousRelease(); + //set block to invalid + block = IntPtr.Zero; + return result; + } + ///<inheritdoc/> + ///<exception cref="OutOfMemoryException"></exception> + ///<exception cref="ObjectDisposedException"></exception> + public void Resize(ref LPVOID block, size_t elements, size_t size, bool zero) + { + //wait for lock + HeapLock.Wait(); + /* + * Realloc may return a null pointer if allocation fails + * so check the results and only assign the block pointer + * if the result is valid. Otherwise pointer block should + * be left untouched + */ + LPVOID newBlock = ReAllocBlock(block, elements, size, zero); + //release lock + HeapLock.Release(); + //Check block + if (newBlock == IntPtr.Zero) + { + throw new NativeMemoryOutOfMemoryException("The memory block could not be resized"); + } + //Set the new block + block = newBlock; + } + + ///<inheritdoc/> + protected override bool ReleaseHandle() + { + HeapLock.Dispose(); + return true; + } + + /// <summary> + /// Allocates a block of memory from the heap + /// </summary> + /// <param name="elements">The number of elements within the block</param> + /// <param name="size">The size of the element type (in bytes)</param> + /// <param name="zero">A flag to zero the allocated block</param> + /// <returns>A pointer to the allocated block</returns> + protected abstract LPVOID AllocBlock(size_t elements, size_t size, bool zero); + /// <summary> + /// Frees a previously allocated block of memory + /// </summary> + /// <param name="block">The block to free</param> + protected abstract bool FreeBlock(LPVOID block); + /// <summary> + /// Resizes the previously allocated block of memory on the current heap + /// </summary> + /// <param name="block">The prevously allocated block</param> + /// <param name="elements">The new number of elements within the block</param> + /// <param name="size">The size of the element type (in bytes)</param> + /// <param name="zero">A flag to indicate if the new region of the block should be zeroed</param> + /// <returns>A pointer to the same block, but resized, null if the allocation falied</returns> + /// <remarks> + /// Heap base relies on the block pointer to remain unchanged if the resize fails so the + /// block is still valid, and the return value is used to determine if the resize was successful + /// </remarks> + protected abstract LPVOID ReAllocBlock(LPVOID block, size_t elements, size_t size, bool zero); + ///<inheritdoc/> + public override int GetHashCode() => handle.GetHashCode(); + ///<inheritdoc/> + public override bool Equals(object? obj) + { + return obj is UnmanagedHeapBase heap && !heap.IsInvalid && !heap.IsClosed && handle == heap.handle; + } + } +} diff --git a/Utils/src/Memory/UnsafeMemoryHandle.cs b/Utils/src/Memory/UnsafeMemoryHandle.cs new file mode 100644 index 0000000..b05ad40 --- /dev/null +++ b/Utils/src/Memory/UnsafeMemoryHandle.cs @@ -0,0 +1,231 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: UnsafeMemoryHandle.cs +* +* UnsafeMemoryHandle.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; +using System.Buffers; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// Represents an unsafe handle to managed/unmanaged memory that should be used cautiously. + /// A referrence counter is not maintained. + /// </summary> + /// <typeparam name="T">Unmanaged memory type</typeparam> + [StructLayout(LayoutKind.Sequential)] + public readonly struct UnsafeMemoryHandle<T> : IMemoryHandle<T>, IEquatable<UnsafeMemoryHandle<T>> where T : unmanaged + { + private enum HandleType + { + None, + Pool, + PrivateHeap + } + + private readonly T[]? _poolArr; + private readonly IntPtr _memoryPtr; + private readonly ArrayPool<T>? _pool; + private readonly IUnmangedHeap? _heap; + private readonly HandleType _handleType; + private readonly int _length; + + ///<inheritdoc/> + public readonly unsafe Span<T> Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _handleType == HandleType.Pool ? _poolArr.AsSpan(0, IntLength) : new (_memoryPtr.ToPointer(), IntLength); + } + ///<inheritdoc/> + public readonly int IntLength => _length; + ///<inheritdoc/> + public readonly ulong Length => (ulong)_length; + + /// <summary> + /// Creates an empty <see cref="UnsafeMemoryHandle{T}"/> + /// </summary> + public UnsafeMemoryHandle() + { + _pool = null; + _heap = null; + _poolArr = null; + _memoryPtr = IntPtr.Zero; + _handleType = HandleType.None; + _length = 0; + } + + /// <summary> + /// Inializes a new <see cref="UnsafeMemoryHandle{T}"/> using the specified + /// <see cref="ArrayPool{T}"/> + /// </summary> + /// <param name="elements">The number of elements to store</param> + /// <param name="zero">Zero initial contents?</param> + /// <param name="pool">The explicit pool to alloc buffers from</param> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public unsafe UnsafeMemoryHandle(ArrayPool<T> pool, int elements, bool zero) + { + if (elements < 0) + { + throw new ArgumentOutOfRangeException(nameof(elements)); + } + //Pool is required + _pool = pool ?? throw new ArgumentNullException(nameof(pool)); + //Rent the array from the pool and hold referrence to it + _poolArr = pool.Rent(elements, zero); + //Cant store ref to array becase GC can move it + _memoryPtr = IntPtr.Zero; + //Set pool handle type + _handleType = HandleType.Pool; + //No heap being loaded + _heap = null; + _length = elements; + } + + /// <summary> + /// Intializes a new <see cref="UnsafeMemoryHandle{T}"/> for block of memory allocated from + /// an <see cref="IUnmangedHeap"/> + /// </summary> + /// <param name="heap">The heap the initial memory block belongs to</param> + /// <param name="initial">A pointer to the unmanaged memory block</param> + /// <param name="elements">The number of elements this block points to</param> + internal UnsafeMemoryHandle(IUnmangedHeap heap, IntPtr initial, int elements) + { + _pool = null; + _poolArr = null; + _heap = heap; + _length = elements; + _memoryPtr = initial; + _handleType = HandleType.PrivateHeap; + } + + /// <summary> + /// Releases memory back to the pool or heap from which is was allocated. + /// </summary> + /// <remarks>After this method is called, this handle points to invalid memory</remarks> + public readonly void Dispose() + { + switch (_handleType) + { + case HandleType.Pool: + { + //Return array to pool + _pool!.Return(_poolArr!); + } + break; + case HandleType.PrivateHeap: + { + IntPtr unalloc = _memoryPtr; + //Free the unmanaged handle + _heap!.Free(ref unalloc); + } + break; + } + } + + ///<inheritdoc/> + public readonly override int GetHashCode() => _handleType == HandleType.Pool ? _poolArr!.GetHashCode() : _memoryPtr.GetHashCode(); + ///<inheritdoc/> + public readonly unsafe MemoryHandle Pin(int elementIndex) + { + //Guard + if (elementIndex < 0 || elementIndex >= IntLength) + { + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + } + + if (_handleType == HandleType.Pool) + { + //Pin the array + GCHandle arrHandle = GCHandle.Alloc(_poolArr, GCHandleType.Pinned); + //Get array base address + void* basePtr = (void*)arrHandle.AddrOfPinnedObject(); + //Get element offset + void* indexOffet = Unsafe.Add<T>(basePtr, elementIndex); + return new (indexOffet, arrHandle); + } + else + { + //Get offset pointer and pass self as pinnable argument, (nothing happens but support it) + void* basePtr = Unsafe.Add<T>(_memoryPtr.ToPointer(), elementIndex); + //Unmanaged memory is always pinned, so no need to pass this as IPinnable, since it will cause a box + return new (basePtr); + } + } + ///<inheritdoc/> + public readonly void Unpin() + { + //Nothing to do since gc handle takes care of array, and unmanaged pointers are not pinned + } + + /// <summary> + /// Determines if the other handle represents the same memory block as the + /// current handle. + /// </summary> + /// <param name="other">The other handle to test</param> + /// <returns>True if the other handle points to the same block of memory as the current handle</returns> + public readonly bool Equals(UnsafeMemoryHandle<T> other) + { + return _handleType == other._handleType && Length == other.Length && GetHashCode() == other.GetHashCode(); + } + + /// <summary> + /// Override for object equality operator, will cause boxing + /// for structures + /// </summary> + /// <param name="obj">The other object to compare</param> + /// <returns> + /// True if the passed object is of type <see cref="UnsafeMemoryHandle{T}"/> + /// and uses the structure equality operator + /// false otherwise. + /// </returns> + public readonly override bool Equals([NotNullWhen(true)] object? obj) => obj is UnsafeMemoryHandle<T> other && Equals(other); + + /// <summary> + /// Casts the handle to it's <see cref="Span{T}"/> representation + /// </summary> + /// <param name="handle">the handle to cast</param> + public static implicit operator Span<T>(in UnsafeMemoryHandle<T> handle) => handle.Span; + + /// <summary> + /// Equality overload + /// </summary> + /// <param name="left"></param> + /// <param name="right"></param> + /// <returns>True if handles are equal, flase otherwise</returns> + public static bool operator ==(in UnsafeMemoryHandle<T> left, in UnsafeMemoryHandle<T> right) => left.Equals(right); + /// <summary> + /// Equality overload + /// </summary> + /// <param name="left"></param> + /// <param name="right"></param> + /// <returns>True if handles are equal, flase otherwise</returns> + public static bool operator !=(in UnsafeMemoryHandle<T> left, in UnsafeMemoryHandle<T> right) => !left.Equals(right); + + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/VnString.cs b/Utils/src/Memory/VnString.cs new file mode 100644 index 0000000..7fa0c5a --- /dev/null +++ b/Utils/src/Memory/VnString.cs @@ -0,0 +1,497 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnString.cs +* +* VnString.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; +using System.IO; +using System.Text; +using System.Buffers; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +using VNLib.Utils.IO; +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.Memory +{ + + /// <summary> + /// Provides an immutable character buffer stored on an unmanged heap. Contains handles to unmanged memory, and should be disposed + /// </summary> + [ComVisible(false)] + [ImmutableObject(true)] + public sealed class VnString : VnDisposeable, IEquatable<VnString>, IEquatable<string>, IEquatable<char[]>, IComparable<VnString>, IComparable<string> + { + private readonly MemoryHandle<char>? Handle; + + private readonly SubSequence<char> _stringSequence; + + /// <summary> + /// The number of unicode characters the current instance can reference + /// </summary> + public int Length => _stringSequence.Size; + /// <summary> + /// Gets a value indicating if the current instance is empty + /// </summary> + public bool IsEmpty => Length == 0; + + private VnString(SubSequence<char> sequence) + { + _stringSequence = sequence; + } + + private VnString( + MemoryHandle<char> handle, +#if TARGET_64_BIT + ulong start, +#else + int start, +#endif + int length) + { + Handle = handle ?? throw new ArgumentNullException(nameof(handle)); + //get sequence + _stringSequence = handle.GetSubSequence(start, length); + } + + /// <summary> + /// Creates and empty <see cref="VnString"/>, not particularly usefull, just and empty instance. + /// </summary> + public VnString() + { + //Default string sequence is empty and does not hold any memory + } + /// <summary> + /// Creates a new <see cref="VnString"/> around a <see cref="ReadOnlySpan{T}"/> or a <see cref="string"/> of data + /// </summary> + /// <param name="data"><see cref="ReadOnlySpan{T}"/> of data to replicate</param> + /// <exception cref="OutOfMemoryException"></exception> + public VnString(ReadOnlySpan<char> data) + { + //Create new handle with enough size (heap) + Handle = Memory.Shared.Alloc<char>(data.Length); + //Copy + Memory.Copy(data, Handle, 0); + //Get subsequence over the whole copy of data + _stringSequence = Handle.GetSubSequence(0, data.Length); + } + /// <summary> + /// Allocates a temporary buffer to read data from the stream until the end of the stream is reached. + /// Decodes data from the user-specified encoding + /// </summary> + /// <param name="stream">Active stream of data to decode to a string</param> + /// <param name="encoding"><see cref="Encoding"/> to use for decoding</param> + /// <param name="bufferSize">The size of the buffer to allocate during copying</param> + /// <returns>The new <see cref="VnString"/> instance</returns> + /// <exception cref="IOException"></exception> + /// <exception cref="OverflowException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="InvalidOperationException"></exception> + public static VnString FromStream(Stream stream, Encoding encoding, uint bufferSize) + { + //Make sure the stream is readable + if (!stream.CanRead) + { + throw new InvalidOperationException(); + } + //See if the stream is a vn memory stream + if (stream is VnMemoryStream vnms) + { + //Get the number of characters + int numChars = encoding.GetCharCount(vnms.AsSpan()); + //New handle + MemoryHandle<char> charBuffer = Memory.Shared.Alloc<char>(numChars); + try + { + //Write characters to character buffer + _ = encoding.GetChars(vnms.AsSpan(), charBuffer); + //Consume the new handle + return ConsumeHandle(charBuffer, 0, numChars); + } + catch + { + //If an error occured, dispose the buffer + charBuffer.Dispose(); + throw; + } + } + //Need to read from the stream old school with buffers + else + { + //Create a new char bufer that will expand dyanmically + MemoryHandle<char> charBuffer = Memory.Shared.Alloc<char>(bufferSize); + //Allocate a binary buffer + MemoryHandle<byte> binBuffer = Memory.Shared.Alloc<byte>(bufferSize); + try + { + int length = 0; + //span ref to bin buffer + Span<byte> buffer = binBuffer; + //Run in checked context for overflows + checked + { + do + { + //read + int read = stream.Read(buffer); + //guard + if (read <= 0) + { + break; + } + //Slice into only the read data + ReadOnlySpan<byte> readbytes = buffer[..read]; + //get num chars + int numChars = encoding.GetCharCount(readbytes); + //Guard for overflow + if (((ulong)(numChars + length)) >= int.MaxValue) + { + throw new OverflowException(); + } + //Re-alloc buffer + charBuffer.ResizeIfSmaller(length + numChars); + //Decode and update position + _= encoding.GetChars(readbytes, charBuffer.Span.Slice(length, numChars)); + //Update char count + length += numChars; + } while (true); + } + return ConsumeHandle(charBuffer, 0, length); + } + catch + { + //Free the memory allocated + charBuffer.Dispose(); + //We still want the exception to be propagated! + throw; + } + finally + { + //Dispose the binary buffer + binBuffer.Dispose(); + } + } + } + /// <summary> + /// Creates a new Vnstring from the <see cref="MemoryHandle{T}"/> buffer provided. This function "consumes" + /// a handle, meaning it now takes ownsership of the the memory it points to. + /// </summary> + /// <param name="handle">The <see cref="MemoryHandle{T}"/> to consume</param> + /// <param name="start">The offset from the begining of the buffer marking the begining of the string</param> + /// <param name="length">The number of characters this string points to</param> + /// <returns>The new <see cref="VnString"/></returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static VnString ConsumeHandle( + MemoryHandle<char> handle, + +#if TARGET_64_BIT + ulong start, +#else + int start, +#endif + + int length) + { + if(length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + if((uint)length > handle.Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + return new VnString(handle, start, length); + } + /// <summary> + /// Asynchronously reads data from the specified stream and uses the specified encoding + /// to decode the binary data to a new <see cref="VnString"/> heap character buffer. + /// </summary> + /// <param name="stream">The stream to read data from</param> + /// <param name="encoding">The encoding to use while decoding data</param> + /// <param name="heap">The <see cref="IUnmangedHeap"/> to allocate buffers from</param> + /// <param name="bufferSize">The size of the buffer to allocate</param> + /// <returns>The new <see cref="VnString"/> containing the data</returns> + /// <exception cref="IOException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + public static async ValueTask<VnString> FromStreamAsync(Stream stream, Encoding encoding, IUnmangedHeap heap, int bufferSize) + { + _ = stream ?? throw new ArgumentNullException(nameof(stream)); + _ = encoding ?? throw new ArgumentNullException(nameof(encoding)); + _ = heap ?? throw new ArgumentNullException(nameof(heap)); + + //Make sure the stream is readable + if (!stream.CanRead) + { + throw new IOException("The input stream is not readable"); + } + //See if the stream is a vn memory stream + if (stream is VnMemoryStream vnms) + { + //Get the number of characters + int numChars = encoding.GetCharCount(vnms.AsSpan()); + //New handle + MemoryHandle<char> charBuffer = heap.Alloc<char>(numChars); + try + { + //Write characters to character buffer + _ = encoding.GetChars(vnms.AsSpan(), charBuffer); + //Consume the new handle + return ConsumeHandle(charBuffer, 0, numChars); + } + catch + { + //If an error occured, dispose the buffer + charBuffer.Dispose(); + throw; + } + } + else + { + //Create a new char bufer starting with the buffer size + MemoryHandle<char> charBuffer = heap.Alloc<char>(bufferSize); + //Rent a temp binary buffer + IMemoryOwner<byte> binBuffer = heap.DirectAlloc<byte>(bufferSize); + try + { + int length = 0; + do + { + //read async + int read = await stream.ReadAsync(binBuffer.Memory); + //guard + if (read <= 0) + { + break; + } + //calculate the number of characters + int numChars = encoding.GetCharCount(binBuffer.Memory.Span[..read]); + //Guard for overflow + if (((ulong)(numChars + length)) >= int.MaxValue) + { + throw new OverflowException(); + } + //Re-alloc buffer + charBuffer.ResizeIfSmaller(length + numChars); + //Decode and update position + _ = encoding.GetChars(binBuffer.Memory.Span[..read], charBuffer.Span.Slice(length, numChars)); + //Update char count + length += numChars; + } while (true); + return ConsumeHandle(charBuffer, 0, length); + } + catch + { + //Free the memory allocated + charBuffer.Dispose(); + //We still want the exception to be propagated! + throw; + } + finally + { + //Dispose the binary buffer + binBuffer.Dispose(); + } + } + } + + /// <summary> + /// Gets the value of the character at the specified index + /// </summary> + /// <param name="index">The index of the character to get</param> + /// <returns>The <see cref="char"/> at the specified index within the buffer</returns> + /// <exception cref="ObjectDisposedException"></exception> + public char CharAt(int index) + { + //Check + Check(); + //Check bounds + return _stringSequence.Span[index]; + } + +#pragma warning disable IDE0057 // Use range operator + /// <summary> + /// Creates a <see cref="VnString"/> that is a window within the current string, + /// the referrence points to the same memory as the first instnace. + /// </summary> + /// <param name="start">The index within the current string to begin the child string</param> + /// <param name="count">The number of characters (or length) of the child string</param> + /// <returns>The child <see cref="VnString"/></returns> + /// <remarks> + /// Making substrings will reference the parents's underlying <see cref="MemoryHandle{T}"/> + /// and all children will be set in a disposed state when the parent instance is disposed + /// </remarks> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public VnString Substring(int start, int count) + { + //Check + Check(); + //Check bounds + if (start < 0 || (start + count) >= Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + //get sub-sequence slice for the current string + SubSequence<char> sub = _stringSequence.Slice((uint)start, count); + //Create new string with offsets pointing to same internal referrence + return new VnString(sub); + } + /// <summary> + /// Creates a <see cref="VnString"/> that is a window within the current string, + /// the referrence points to the same memory as the first instnace. + /// </summary> + /// <param name="start">The index within the current string to begin the child string</param> + /// <returns>The child <see cref="VnString"/></returns> + /// <remarks> + /// Making substrings will reference the parents's underlying <see cref="MemoryHandle{T}"/> + /// and all children will be set in a disposed state when the parent instance is disposed + /// </remarks> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public VnString Substring(int start) => Substring(start, (Length - start)); + public VnString this[Range range] + { + get + { + //get start + int start = range.Start.IsFromEnd ? Length - range.Start.Value : range.Start.Value; + //Get end + int end = range.End.IsFromEnd ? Length - range.End.Value : range.End.Value; + //Handle strings with no ending range + return (end >= start) ? Substring(start, (end - start)) : Substring(start); + } + } +#pragma warning restore IDE0057 // Use range operator + + /// <summary> + /// Gets a <see cref="ReadOnlySpan{T}"/> over the internal character buffer + /// </summary> + /// <returns></returns> + /// <exception cref="ObjectDisposedException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan<char> AsSpan() + { + //Check + Check(); + return _stringSequence.Span; + } + + /// <summary> + /// Gets a <see cref="string"/> copy of the internal buffer + /// </summary> + /// <returns><see cref="string"/> representation of internal data</returns> + /// <exception cref="ObjectDisposedException"></exception> + public override unsafe string ToString() + { + //Check + Check(); + //Create a new + return AsSpan().ToString(); + } + /// <summary> + /// Gets the value of the character at the specified index + /// </summary> + /// <param name="index">The index of the character to get</param> + /// <returns>The <see cref="char"/> at the specified index within the buffer</returns> + /// <exception cref="ObjectDisposedException"></exception> + public char this[int index] => CharAt(index); + + //Casting to a vnstring should be explicit so the caller doesnt misuse memory managment + public static explicit operator ReadOnlySpan<char>(VnString? value) => Unsafe.IsNullRef(ref value) || value!.Disposed ? ReadOnlySpan<char>.Empty : value.AsSpan(); + public static explicit operator VnString(string value) => new (value); + public static explicit operator VnString(ReadOnlySpan<char> value) => new (value); + public static explicit operator VnString(char[] value) => new (value); + ///<inheritdoc/> + public override bool Equals(object? obj) + { + if(obj == null) + { + return false; + } + return obj switch + { + VnString => Equals(obj as VnString), //Use operator overload + string => Equals(obj as string), //Use operator overload + char[] => Equals(obj as char[]), //Use operator overload + _ => false, + }; + } + ///<inheritdoc/> + public bool Equals(VnString? other) => !ReferenceEquals(other, null) && Equals(other.AsSpan()); + ///<inheritdoc/> + public bool Equals(VnString? other, StringComparison stringComparison) => !ReferenceEquals(other, null) && Equals(other.AsSpan(), stringComparison); + ///<inheritdoc/> + public bool Equals(string? other) => Equals(other.AsSpan()); + ///<inheritdoc/> + public bool Equals(string? other, StringComparison stringComparison) => Equals(other.AsSpan(), stringComparison); + ///<inheritdoc/> + public bool Equals(char[]? other) => Equals(other.AsSpan()); + ///<inheritdoc/> + public bool Equals(char[]? other, StringComparison stringComparison) => Equals(other.AsSpan(), stringComparison); + ///<inheritdoc/> + public bool Equals(ReadOnlySpan<char> other, StringComparison stringComparison = StringComparison.Ordinal) => Length == other.Length && AsSpan().Equals(other, stringComparison); + ///<inheritdoc/> + public bool Equals(SubSequence<char> other) => Length == other.Size && AsSpan().SequenceEqual(other.Span); + ///<inheritdoc/> + public int CompareTo(string? other) => AsSpan().CompareTo(other, StringComparison.Ordinal); + ///<inheritdoc/> + ///<exception cref="ArgumentNullException"></exception> + public int CompareTo(VnString? other) + { + _ = other ?? throw new ArgumentNullException(nameof(other)); + return AsSpan().CompareTo(other.AsSpan(), StringComparison.Ordinal); + } + + /// <summary> + /// Gets a hashcode for the underyling string by using the .NET <see cref="string.GetHashCode()"/> + /// method on the character representation of the data + /// </summary> + /// <returns></returns> + /// <remarks> + /// It is safe to compare hashcodes of <see cref="VnString"/> to the <see cref="string"/> class or + /// a character span etc + /// </remarks> + /// <exception cref="ObjectDisposedException"></exception> + public override int GetHashCode() => string.GetHashCode(AsSpan()); + ///<inheritdoc/> + protected override void Free() + { + //Dispose the handle if we own it (null if we do not have the parent handle) + Handle?.Dispose(); + } + + public static bool operator ==(VnString left, VnString right) => ReferenceEquals(left, null) ? ReferenceEquals(right, null) : left.Equals(right); + + public static bool operator !=(VnString left, VnString right) => !(left == right); + + public static bool operator <(VnString left, VnString right) => ReferenceEquals(left, null) ? !ReferenceEquals(right, null) : left.CompareTo(right) < 0; + + public static bool operator <=(VnString left, VnString right) => ReferenceEquals(left, null) || left.CompareTo(right) <= 0; + + public static bool operator >(VnString left, VnString right) => !ReferenceEquals(left, null) && left.CompareTo(right) > 0; + + public static bool operator >=(VnString left, VnString right) => ReferenceEquals(left, null) ? ReferenceEquals(right, null) : left.CompareTo(right) >= 0; + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/VnTable.cs b/Utils/src/Memory/VnTable.cs new file mode 100644 index 0000000..1d5c0a6 --- /dev/null +++ b/Utils/src/Memory/VnTable.cs @@ -0,0 +1,213 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnTable.cs +* +* VnTable.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; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// Provides a Row-Major ordered table for use of storing value-types in umnaged heap memory + /// </summary> + /// <typeparam name="T"></typeparam> + public sealed class VnTable<T> : VnDisposeable, IIndexable<uint, T> where T : unmanaged + { + private readonly MemoryHandle<T>? BufferHandle; + /// <summary> + /// A value that indicates if the table does not contain any values + /// </summary> + public bool Empty { get; } + /// <summary> + /// The number of rows in the table + /// </summary> + public int Rows { get; } + /// <summary> + /// The nuber of columns in the table + /// </summary> + public int Cols { get; } + /// <summary> + /// Creates a new 2 dimensional table in unmanaged heap memory, using the <see cref="Memory.Shared"/> heap. + /// User should dispose of the table when no longer in use + /// </summary> + /// <param name="rows">Number of rows in the table</param> + /// <param name="cols">Number of columns in the table</param> + public VnTable(int rows, int cols) : this(Memory.Shared, rows, cols) { } + /// <summary> + /// Creates a new 2 dimensional table in unmanaged heap memory, using the specified heap. + /// User should dispose of the table when no longer in use + /// </summary> + /// <param name="heap"><see cref="PrivateHeap"/> to allocate table memory from</param> + /// <param name="rows">Number of rows in the table</param> + /// <param name="cols">Number of columns in the table</param> + public VnTable(IUnmangedHeap heap, int rows, int cols) + { + if (rows < 0 || cols < 0) + { + throw new ArgumentOutOfRangeException(nameof(rows), "Row and coulmn number must be 0 or larger"); + } + //empty table + if (rows == 0 && cols == 0) + { + Empty = true; + return; + } + + _ = heap ?? throw new ArgumentNullException(nameof(heap)); + + this.Rows = rows; + this.Cols = cols; + + long tableSize = Math.BigMul(rows, cols); + + //Alloc a buffer with zero memory enabled, with Rows * Cols number of elements + BufferHandle = heap.Alloc<T>(tableSize, true); + } + /// <summary> + /// Gets the value of an item in the table at the given indexes + /// </summary> + /// <param name="row">Row address of the item</param> + /// <param name="col">Column address of item</param> + /// <returns>The value of the item</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="InvalidOperationException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public T Get(int row, int col) + { + Check(); + if (this.Empty) + { + throw new InvalidOperationException("Table is empty"); + } + if (row < 0 || col < 0) + { + throw new ArgumentOutOfRangeException(nameof(row), "Row or column address less than 0"); + } + if (row > this.Rows) + { + throw new ArgumentOutOfRangeException(nameof(row), "Row out of range of current table"); + } + if (col > this.Cols) + { + throw new ArgumentOutOfRangeException(nameof(col), "Column address out of range of current table"); + } + //Calculate the address in memory for the item + //Calc row offset + long address = Cols * row; + //Calc column offset + address += col; + unsafe + { + //Get the value item + return *(BufferHandle!.GetOffset(address)); + } + } + /// <summary> + /// Sets the value of an item in the table at the given address + /// </summary> + /// <param name="item">Value of item to store</param> + /// <param name="row">Row address of the item</param> + /// <param name="col">Column address of item</param> + /// <returns>The value of the item</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="InvalidOperationException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public void Set(int row, int col, T item) + { + Check(); + if (this.Empty) + { + throw new InvalidOperationException("Table is empty"); + } + if (row < 0 || col < 0) + { + throw new ArgumentOutOfRangeException(nameof(row), "Row or column address less than 0"); + } + if (row > this.Rows) + { + throw new ArgumentOutOfRangeException(nameof(row), "Row out of range of current table"); + } + if (col > this.Cols) + { + throw new ArgumentOutOfRangeException(nameof(col), "Column address out of range of current table"); + } + //Calculate the address in memory for the item + //Calc row offset + long address = Cols * row; + //Calc column offset + address += col; + //Set the value item + unsafe + { + *BufferHandle!.GetOffset(address) = item; + } + } + /// <summary> + /// Equivalent to <see cref="VnTable{T}.Get(int, int)"/> and <see cref="VnTable{T}.Set(int, int, T)"/> + /// </summary> + /// <param name="row">Row address of item</param> + /// <param name="col">Column address of item</param> + /// <returns>The value of the item</returns> + public T this[int row, int col] + { + get => Get(row, col); + set => Set(row, col, value); + } + /// <summary> + /// Allows for direct addressing in the table. + /// </summary> + /// <param name="index"></param> + /// <returns></returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="InvalidOperationException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public unsafe T this[uint index] + { + get + { + Check(); + return !Empty ? *(BufferHandle!.GetOffset(index)) : throw new InvalidOperationException("Cannot index an empty table"); + } + + set + { + Check(); + if (Empty) + { + throw new InvalidOperationException("Cannot index an empty table"); + } + *(BufferHandle!.GetOffset(index)) = value; + } + } + ///<inheritdoc/> + protected override void Free() + { + if (!this.Empty) + { + //Dispose the buffer + BufferHandle!.Dispose(); + } + } + } +}
\ No newline at end of file diff --git a/Utils/src/Memory/VnTempBuffer.cs b/Utils/src/Memory/VnTempBuffer.cs new file mode 100644 index 0000000..7726fe1 --- /dev/null +++ b/Utils/src/Memory/VnTempBuffer.cs @@ -0,0 +1,207 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnTempBuffer.cs +* +* VnTempBuffer.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; +using System.Buffers; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +using VNLib.Utils.Extensions; +using System.Security.Cryptography; + +namespace VNLib.Utils.Memory +{ + /// <summary> + /// 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> + { + private readonly ArrayPool<T> Pool; + + /// <summary> + /// Referrence to internal buffer + /// </summary> + public T[] Buffer { get; private set; } + /// <summary> + /// Inital/desired size of internal buffer + /// </summary> + public int InitSize { get; } + + /// <summary> + /// Actual length of internal buffer + /// </summary> + public ulong Length => (ulong)Buffer.LongLength; + + /// <summary> + /// Actual length of internal buffer + /// </summary> + public int IntLength => Buffer.Length; + + ///<inheritdoc/> + ///<exception cref="ObjectDisposedException"></exception> + public Span<T> Span + { + get + { + Check(); + return new Span<T>(Buffer, 0, InitSize); + } + } + + /// <summary> + /// Allocates a new <see cref="VnTempBuffer{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) + {} + /// <summary> + /// Allocates a new <see cref="VnTempBuffer{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) + { + Pool = pool; + Buffer = pool.Rent(minSize, zero); + InitSize = minSize; + } + /// <summary> + /// Gets an offset wrapper around the current buffer + /// </summary> + /// <param name="offset">Offset from begining of current buffer</param> + /// <param name="count">Number of <typeparamref name="T"/> from offset</param> + /// <returns>An <see cref="ArraySegment{BufType}"/> wrapper around the current buffer containing the offset</returns> + public ArraySegment<T> GetOffsetWrapper(int offset, int count) + { + Check(); + //Let arraysegment throw exceptions for checks + return new ArraySegment<T>(Buffer, offset, count); + } + ///<inheritdoc/> + public T this[int index] + { + get + { + Check(); + return Buffer[index]; + } + set + { + Check(); + Buffer[index] = value; + } + } + + /// <summary> + /// Gets a memory structure around the internal buffer + /// </summary> + /// <returns>A memory structure over the buffer</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public Memory<T> AsMemory() + { + 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); + } + /// <summary> + /// Gets a memory structure around the internal buffer + /// </summary> + /// <param name="count">The number of elements included in the result</param> + /// <returns>A memory structure over the buffer</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public Memory<T> AsMemory(int count) + { + Check(); + return new Memory<T>(Buffer, 0, count); + } + + /* + * 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> + /// Returns buffer to shared array-pool + /// </summary> + protected override void Free() + { + //Return the buffer to the array pool + Pool.Return(Buffer); + + //Set buffer to null, +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + Buffer = null; +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } + + unsafe MemoryHandle IPinnable.Pin(int elementIndex) + { + //Guard + if (elementIndex < 0 || elementIndex >= IntLength) + { + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + } + + //Pin the array + GCHandle arrHandle = GCHandle.Alloc(Buffer, GCHandleType.Pinned); + //Get array base address + void* basePtr = (void*)arrHandle.AddrOfPinnedObject(); + //Get element offset + void* indexOffet = Unsafe.Add<T>(basePtr, elementIndex); + return new(indexOffet, arrHandle, this); + } + + void IPinnable.Unpin() + { + //Gchandle will manage the unpin + } + + ~VnTempBuffer() => Free(); + + + } +}
\ No newline at end of file diff --git a/Utils/src/Native/SafeLibraryHandle.cs b/Utils/src/Native/SafeLibraryHandle.cs new file mode 100644 index 0000000..4772bd4 --- /dev/null +++ b/Utils/src/Native/SafeLibraryHandle.cs @@ -0,0 +1,220 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: SafeLibraryHandle.cs +* +* SafeLibraryHandle.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; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.Native +{ + /// <summary> + /// Represents a safe handle to a native library loaded to the current process + /// </summary> + public sealed class SafeLibraryHandle : SafeHandle + { + ///<inheritdoc/> + public override bool IsInvalid => handle == IntPtr.Zero; + + private SafeLibraryHandle(IntPtr libHandle) : base(IntPtr.Zero, true) + { + //Init handle + SetHandle(libHandle); + } + + /// <summary> + /// Finds and loads the specified native libary into the current process by its name at runtime + /// </summary> + /// <param name="libPath">The path (or name of libary) to search for</param> + /// <param name="searchPath"> + /// The <see cref="DllImportSearchPath"/> used to search for libaries + /// within the current filesystem + /// </param> + /// <returns>The loaded <see cref="SafeLibraryHandle"/></returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="DllNotFoundException"></exception> + public static SafeLibraryHandle LoadLibrary(string libPath, DllImportSearchPath searchPath = DllImportSearchPath.ApplicationDirectory) + { + _ = libPath ?? throw new ArgumentNullException(nameof(libPath)); + //See if the path includes a file extension + return TryLoadLibrary(libPath, searchPath, out SafeLibraryHandle? lib) + ? lib + : throw new DllNotFoundException($"The library {libPath} or one of its dependencies could not be found"); + } + + /// <summary> + /// Attempts to load the specified native libary into the current process by its name at runtime + /// </summary> + ///<param name="libPath">The path (or name of libary) to search for</param> + /// <param name="searchPath"> + /// The <see cref="DllImportSearchPath"/> used to search for libaries + /// within the current filesystem + /// </param> + /// <param name="lib">The handle to the libary if successfully loaded</param> + /// <returns>True if the libary was found and loaded into the current process</returns> + public static bool TryLoadLibrary(string libPath, DllImportSearchPath searchPath, [NotNullWhen(true)] out SafeLibraryHandle? lib) + { + lib = null; + //Allow full rooted paths + if (Path.IsPathRooted(libPath)) + { + //Attempt a native load + if (NativeLibrary.TryLoad(libPath, out IntPtr libHandle)) + { + lib = new(libHandle); + return true; + } + return false; + } + //Check application directory first (including subdirectories) + if ((searchPath & DllImportSearchPath.ApplicationDirectory) > 0) + { + //get the current directory + string libDir = Directory.GetCurrentDirectory(); + if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib)) + { + return true; + } + } + //See if search in the calling assembly directory + if ((searchPath & DllImportSearchPath.AssemblyDirectory) > 0) + { + //Get the calling assmblies directory + string libDir = Assembly.GetCallingAssembly().Location; + Debug.WriteLine("Native library searching for calling assembly location:{0} ", libDir); + if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib)) + { + return true; + } + } + //Search system32 dir + if ((searchPath & DllImportSearchPath.System32) > 0) + { + //Get the system directory + string libDir = Environment.GetFolderPath(Environment.SpecialFolder.SystemX86); + if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib)) + { + return true; + } + } + //Attempt a native load + { + if (NativeLibrary.TryLoad(libPath, out IntPtr libHandle)) + { + lib = new(libHandle); + return true; + } + return false; + } + } + + private static bool TryLoadLibraryInternal(string libDir, string libPath, SearchOption dirSearchOptions, [NotNullWhen(true)] out SafeLibraryHandle? libary) + { + //Try to find the libary file + string? libFile = GetLibraryFile(libDir, libPath, dirSearchOptions); + //Load libary + if (libFile != null && NativeLibrary.TryLoad(libFile, out IntPtr libHandle)) + { + libary = new SafeLibraryHandle(libHandle); + return true; + } + libary = null; + return false; + } + private static string? GetLibraryFile(string dirPath, string libPath, SearchOption search) + { + //slice the lib to its file name + libPath = Path.GetFileName(libPath); + libPath = Path.ChangeExtension(libPath, OperatingSystem.IsWindows() ? ".dll" : ".so"); + //Select the first file that matches the name + return Directory.EnumerateFiles(dirPath, libPath, search).FirstOrDefault(); + } + + /// <summary> + /// Loads a native method from the library of the specified name and managed delegate + /// </summary> + /// <typeparam name="T">The native method delegate type</typeparam> + /// <param name="methodName">The name of the native method</param> + /// <returns>A wapper handle around the native method delegate</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ObjectDisposedException">If the handle is closed or invalid</exception> + /// <exception cref="EntryPointNotFoundException">When the specified entrypoint could not be found</exception> + public SafeMethodHandle<T> GetMethod<T>(string methodName) where T : Delegate + { + //Increment handle count before obtaining a method + bool success = false; + DangerousAddRef(ref success); + if (!success) + { + throw new ObjectDisposedException("The libary has been released!"); + } + try + { + //Get the method pointer + IntPtr nativeMethod = NativeLibrary.GetExport(handle, methodName); + //Get the delegate for the function pointer + T method = Marshal.GetDelegateForFunctionPointer<T>(nativeMethod); + return new(this, method); + } + catch + { + DangerousRelease(); + throw; + } + } + /// <summary> + /// Gets an delegate wrapper for the specified method without tracking its referrence. + /// The caller must manage the <see cref="SafeLibraryHandle"/> referrence count in order + /// to not leak resources or cause process corruption + /// </summary> + /// <typeparam name="T">The native method delegate type</typeparam> + /// <param name="methodName">The name of the native method</param> + /// <returns>A the delegate wrapper on the native method</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ObjectDisposedException">If the handle is closed or invalid</exception> + /// <exception cref="EntryPointNotFoundException">When the specified entrypoint could not be found</exception> + public T DangerousGetMethod<T>(string methodName) where T : Delegate + { + this.ThrowIfClosed(); + //Get the method pointer + IntPtr nativeMethod = NativeLibrary.GetExport(handle, methodName); + //Get the delegate for the function pointer + return Marshal.GetDelegateForFunctionPointer<T>(nativeMethod); + } + + ///<inheritdoc/> + protected override bool ReleaseHandle() + { + //Free the library and set the handle as invalid + NativeLibrary.Free(handle); + SetHandleAsInvalid(); + return true; + } + } +} diff --git a/Utils/src/Native/SafeMethodHandle.cs b/Utils/src/Native/SafeMethodHandle.cs new file mode 100644 index 0000000..3ba0879 --- /dev/null +++ b/Utils/src/Native/SafeMethodHandle.cs @@ -0,0 +1,61 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: SafeMethodHandle.cs +* +* SafeMethodHandle.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; + +using VNLib.Utils.Resources; + +namespace VNLib.Utils.Native +{ + /// <summary> + /// Represents a handle to a <see cref="SafeLibraryHandle"/>'s + /// native method + /// </summary> + /// <typeparam name="T">The native method deelgate type</typeparam> + public class SafeMethodHandle<T> : OpenHandle where T : Delegate + { + private T? _method; + private readonly SafeLibraryHandle Library; + + internal SafeMethodHandle(SafeLibraryHandle lib, T method) + { + Library = lib; + _method = method; + } + + /// <summary> + /// A delegate to the native method + /// </summary> + public T? Method => _method; + + ///<inheritdoc/> + protected override void Free() + { + //Release the method + _method = default; + //Decrement lib handle count + Library.DangerousRelease(); + } + } +} diff --git a/Utils/src/NativeLibraryException.cs b/Utils/src/NativeLibraryException.cs new file mode 100644 index 0000000..5c66852 --- /dev/null +++ b/Utils/src/NativeLibraryException.cs @@ -0,0 +1,89 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: NativeLibraryException.cs +* +* NativeLibraryException.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 +{ + /// <summary> + /// Raised when an internal buffer was not propery sized for the opreation + /// </summary> + public class InternalBufferTooSmallException : OutOfMemoryException + { + public InternalBufferTooSmallException(string message) : base(message) + {} + + public InternalBufferTooSmallException(string message, Exception innerException) : base(message, innerException) + {} + + public InternalBufferTooSmallException() + {} + } + + /// <summary> + /// A base class for all native library related exceptions + /// </summary> + public class NativeLibraryException : SystemException + { + public NativeLibraryException(string message) : base(message) + {} + + public NativeLibraryException(string message, Exception innerException) : base(message, innerException) + {} + + public NativeLibraryException() + {} + } + + /// <summary> + /// Base exception class for native memory related exceptions + /// </summary> + public class NativeMemoryException : NativeLibraryException + { + public NativeMemoryException(string message) : base(message) + {} + + public NativeMemoryException(string message, Exception innerException) : base(message, innerException) + {} + + public NativeMemoryException() + {} + } + + /// <summary> + /// Raised when a memory allocation or resize failed because there is + /// no more memory available + /// </summary> + public class NativeMemoryOutOfMemoryException : OutOfMemoryException + { + public NativeMemoryOutOfMemoryException(string message) : base(message) + {} + + public NativeMemoryOutOfMemoryException(string message, Exception innerException) : base(message, innerException) + {} + + public NativeMemoryOutOfMemoryException() + {} + } +} diff --git a/Utils/src/Resources/BackedResourceBase.cs b/Utils/src/Resources/BackedResourceBase.cs new file mode 100644 index 0000000..0a2e1e3 --- /dev/null +++ b/Utils/src/Resources/BackedResourceBase.cs @@ -0,0 +1,79 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: BackedResourceBase.cs +* +* BackedResourceBase.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; +using System.Text.Json; +using System.Runtime.CompilerServices; + +namespace VNLib.Utils.Resources +{ + /// <summary> + /// A base class for a resource that is backed by an external data store. + /// Implements the <see cref="IResource"/> interfaceS + /// </summary> + public abstract class BackedResourceBase : IResource + { + ///<inheritdoc/> + public bool IsReleased { get; protected set; } + + /// <summary> + /// Optional <see cref="JsonSerializerOptions"/> to be used when serializing + /// the resource + /// </summary> + internal protected virtual JsonSerializerOptions? JSO { get; } + + /// <summary> + /// A value indicating whether the instance should be deleted when released + /// </summary> + protected bool Deleted { get; set; } + /// <summary> + /// A value indicating whether the instance should be updated when released + /// </summary> + protected bool Modified { get; set; } + + /// <summary> + /// Checks if the resouce has been disposed and raises an exception if it is + /// </summary> + /// <exception cref="ObjectDisposedException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void Check() + { + if (IsReleased) + { + throw new ObjectDisposedException("The resource has been disposed"); + } + } + + /// <summary> + /// Returns the JSON serializable resource to be updated during an update + /// </summary> + /// <returns>The resource to update</returns> + protected abstract object GetResource(); + + /// <summary> + /// Marks the resource for deletion from backing store during closing events + /// </summary> + public virtual void Delete() => Deleted = true; + } +}
\ No newline at end of file diff --git a/Utils/src/Resources/CallbackOpenHandle.cs b/Utils/src/Resources/CallbackOpenHandle.cs new file mode 100644 index 0000000..625bd45 --- /dev/null +++ b/Utils/src/Resources/CallbackOpenHandle.cs @@ -0,0 +1,44 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: CallbackOpenHandle.cs +* +* CallbackOpenHandle.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.Resources +{ + /// <summary> + /// A concrete <see cref="OpenHandle"/> for a defered operation or a resource that should be released or unwound + /// when the instance lifetime has ended. + /// </summary> + public sealed class CallbackOpenHandle : OpenHandle + { + private readonly Action ReleaseFunc; + /// <summary> + /// Creates a new generic <see cref="OpenHandle"/> with the specified release callback method + /// </summary> + /// <param name="release">The callback function to invoke when the <see cref="OpenHandle"/> is disposed</param> + public CallbackOpenHandle(Action release) => ReleaseFunc = release; + ///<inheritdoc/> + protected override void Free() => ReleaseFunc(); + } +}
\ No newline at end of file diff --git a/Utils/src/Resources/ExclusiveResourceHandle.cs b/Utils/src/Resources/ExclusiveResourceHandle.cs new file mode 100644 index 0000000..173bdd1 --- /dev/null +++ b/Utils/src/Resources/ExclusiveResourceHandle.cs @@ -0,0 +1,81 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ExclusiveResourceHandle.cs +* +* ExclusiveResourceHandle.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.Resources +{ + /// <summary> + /// While in scope, holds an exclusive lock on the specified object that implements the <see cref="IExclusiveResource"/> interface + /// </summary> + /// <typeparam name="T"></typeparam> + public class ExclusiveResourceHandle<T> : OpenResourceHandle<T> where T : IExclusiveResource + { + private readonly Action Release; + private readonly Lazy<T> LazyVal; + + /// <summary> + /// <inheritdoc/> + /// <br></br> + /// <br></br> + /// This value is lazy inialized and will invoke the factory function on first access. + /// Accessing this variable is thread safe while the handle is in scope + /// <br></br> + /// <br></br> + /// Exceptions will be propagated during initialziation + /// </summary> + public override T Resource => LazyVal.Value; + + /// <summary> + /// Creates a new <see cref="ExclusiveResourceHandle{TResource}"/> wrapping the + /// <see cref="IExclusiveResource"/> object to manage its lifecycle and reuse + /// </summary> + /// <param name="factory">Factory function that will generate the value when used</param> + /// <param name="release">Callback function that will be invoked after object gets disposed</param> + internal ExclusiveResourceHandle(Func<T> factory, Action release) + { + //Store the release action + Release = release; + //Store the new lazy val from the factory function (enabled thread safey) + LazyVal = new(factory, System.Threading.LazyThreadSafetyMode.ExecutionAndPublication); + } + + protected override void Free() + { + try + { + //Dispose the value if it was created, otherwise do not create it + if (LazyVal.IsValueCreated) + { + Resource?.Release(); + } + } + finally + { + //Always invoke the release callback + Release(); + } + } + } +}
\ No newline at end of file diff --git a/Utils/src/Resources/IExclusiveResource.cs b/Utils/src/Resources/IExclusiveResource.cs new file mode 100644 index 0000000..43ec607 --- /dev/null +++ b/Utils/src/Resources/IExclusiveResource.cs @@ -0,0 +1,39 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IExclusiveResource.cs +* +* IExclusiveResource.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.Resources +{ + + /// <summary> + /// An object, that when used in a mulithreading context, guaruntees that the caller has exclusive + /// access to the instance and relinquishes exclusive access when Release() is called; + /// </summary> + public interface IExclusiveResource : IResource + { + /// <summary> + /// Releases the resource from use. Called when a <see cref="ExclusiveResourceHandle{T}"/> is disposed + /// </summary> + void Release(); + } +}
\ No newline at end of file diff --git a/Utils/src/Resources/IResource.cs b/Utils/src/Resources/IResource.cs new file mode 100644 index 0000000..345e284 --- /dev/null +++ b/Utils/src/Resources/IResource.cs @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IResource.cs +* +* IResource.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.Resources +{ + /// <summary> + /// The base interface for any resource that may be backed by an external + /// data store, and has a finite lifetime, usually accessed within a handle + /// </summary> + public interface IResource + { + /// <summary> + /// Gets a value indicating if the resource has been released + /// </summary> + bool IsReleased { get; } + } +}
\ No newline at end of file diff --git a/Utils/src/Resources/OpenHandle.cs b/Utils/src/Resources/OpenHandle.cs new file mode 100644 index 0000000..6133a65 --- /dev/null +++ b/Utils/src/Resources/OpenHandle.cs @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: OpenHandle.cs +* +* OpenHandle.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.Resources +{ + /// <summary> + /// Represents a base class for an open resource or operation that is valid while being held, + /// and is released or unwound when disposed. + /// </summary> + /// <remarks> + /// The <see cref="OpenHandle"/> pattern, may throw exceptions when disposed as deferred + /// release actions are completed + /// </remarks> + public abstract class OpenHandle : VnDisposeable + { + } +}
\ No newline at end of file diff --git a/Utils/src/Resources/OpenResourceHandle.cs b/Utils/src/Resources/OpenResourceHandle.cs new file mode 100644 index 0000000..d9f9fd2 --- /dev/null +++ b/Utils/src/Resources/OpenResourceHandle.cs @@ -0,0 +1,44 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: OpenResourceHandle.cs +* +* OpenResourceHandle.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.Resources +{ + /// <summary> + /// <para> + /// An abstract base class for an <see cref="OpenHandle"/> that holds a specific resource and manages its lifetime. + /// </para> + /// </summary> + /// <inheritdoc/> + /// <typeparam name="TResource">The resource type</typeparam> + public abstract class OpenResourceHandle<TResource> : OpenHandle + { + /// <summary> + /// The resource held by the open handle + /// </summary> + /// <exception cref="ObjectDisposedException"></exception> + public abstract TResource Resource { get; } + } +}
\ No newline at end of file diff --git a/Utils/src/Resources/ResourceDeleteFailedException.cs b/Utils/src/Resources/ResourceDeleteFailedException.cs new file mode 100644 index 0000000..8e796b5 --- /dev/null +++ b/Utils/src/Resources/ResourceDeleteFailedException.cs @@ -0,0 +1,40 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ResourceDeleteFailedException.cs +* +* ResourceDeleteFailedException.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; +using System.Runtime.Serialization; + +namespace VNLib.Utils.Resources +{ + /// <summary> + /// Raised when a resource delete has failed + /// </summary> + public class ResourceDeleteFailedException : Exception + { + public ResourceDeleteFailedException() { } + public ResourceDeleteFailedException(string message) : base(message) { } + public ResourceDeleteFailedException(string message, Exception innerException) : base(message, innerException) { } + protected ResourceDeleteFailedException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +}
\ No newline at end of file diff --git a/Utils/src/Resources/ResourceUpdateFailedException.cs b/Utils/src/Resources/ResourceUpdateFailedException.cs new file mode 100644 index 0000000..b4b2b3a --- /dev/null +++ b/Utils/src/Resources/ResourceUpdateFailedException.cs @@ -0,0 +1,40 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ResourceUpdateFailedException.cs +* +* ResourceUpdateFailedException.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; +using System.Runtime.Serialization; + +namespace VNLib.Utils.Resources +{ + /// <summary> + /// Raised when a resource update has failed + /// </summary> + public class ResourceUpdateFailedException : Exception + { + public ResourceUpdateFailedException() { } + public ResourceUpdateFailedException(string message) : base(message) { } + public ResourceUpdateFailedException(string message, Exception innerException) : base(message, innerException) { } + protected ResourceUpdateFailedException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +}
\ No newline at end of file diff --git a/Utils/src/Resources/UpdatableResource.cs b/Utils/src/Resources/UpdatableResource.cs new file mode 100644 index 0000000..16f26f2 --- /dev/null +++ b/Utils/src/Resources/UpdatableResource.cs @@ -0,0 +1,113 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: UpdatableResource.cs +* +* UpdatableResource.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; +using System.IO; + +using VNLib.Utils.IO; + +namespace VNLib.Utils.Resources +{ + /// <summary> + /// A callback delegate used for updating a <see cref="UpdatableResource"/> + /// </summary> + /// <param name="source">The <see cref="UpdatableResource"/> to be updated</param> + /// <param name="data">The serialized data to be stored/updated</param> + /// <exception cref="ResourceUpdateFailedException"></exception> + public delegate void UpdateCallback(object source, Stream data); + /// <summary> + /// A callback delegate invoked when a <see cref="UpdatableResource"/> delete is requested + /// </summary> + /// <param name="source">The <see cref="UpdatableResource"/> to be deleted</param> + /// <exception cref="ResourceDeleteFailedException"></exception> + public delegate void DeleteCallback(object source); + + /// <summary> + /// Implemented by a resource that is backed by an external data store, that when modified or deleted will + /// be reflected to the backing store. + /// </summary> + public abstract class UpdatableResource : BackedResourceBase, IExclusiveResource + { + /// <summary> + /// The update callback method to invoke during a release operation + /// when the resource is updated. + /// </summary> + protected abstract UpdateCallback UpdateCb { get; } + /// <summary> + /// The callback method to invoke during a realease operation + /// when the resource should be deleted + /// </summary> + protected abstract DeleteCallback DeleteCb { get; } + + /// <summary> + /// <inheritdoc/> + /// </summary> + /// <exception cref="InvalidOperationException"></exception> + /// <exception cref="ResourceDeleteFailedException"></exception> + /// <exception cref="ResourceUpdateFailedException"></exception> + public virtual void Release() + { + //If resource has already been realeased, return + if (IsReleased) + { + return; + } + //If deleted flag is set, invoke the delete callback + if (Deleted) + { + DeleteCb(this); + } + //If the state has been modifed, flush changes to the store + else if (Modified) + { + FlushPendingChanges(); + } + //Set the released value + IsReleased = true; + } + + /// <summary> + /// Writes the current state of the the resource to the backing store + /// immediatly by invoking the specified callback. + /// <br></br> + /// <br></br> + /// Only call this method if your store supports multiple state updates + /// </summary> + protected virtual void FlushPendingChanges() + { + //Get the resource + object resource = GetResource(); + //Open a memory stream to store data in + using VnMemoryStream data = new(); + //Serialize and write to stream + VnEncoding.JSONSerializeToBinary(resource, data, resource.GetType(), JSO); + //Reset stream to begining + _ = data.Seek(0, SeekOrigin.Begin); + //Invoke update callback + UpdateCb(this, data); + //Clear modified flag + Modified = false; + } + } +}
\ No newline at end of file diff --git a/Utils/src/VNLib.Utils.csproj b/Utils/src/VNLib.Utils.csproj new file mode 100644 index 0000000..b14ab27 --- /dev/null +++ b/Utils/src/VNLib.Utils.csproj @@ -0,0 +1,47 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <RootNamespace>VNLib.Utils</RootNamespace> + <Authors>Vaughn Nugent</Authors> + <Product>VNLib Utilities Library</Product> + <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + <AssemblyName>VNLib.Utils</AssemblyName> + <Version>1.0.1.10</Version> + <Description>Base utilities library, structs, classes</Description> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <Nullable>enable</Nullable> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <AnalysisLevel>latest-all</AnalysisLevel> + <SignAssembly>True</SignAssembly> + <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile> + </PropertyGroup> + + <ItemGroup> + <None Include="..\.editorconfig" Link=".editorconfig" /> + </ItemGroup> + + <PropertyGroup Condition="'$(Configuration)'=='Debug'"> + <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <Deterministic>False</Deterministic> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> + <Deterministic>False</Deterministic> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> +</Project> diff --git a/Utils/src/VnDisposeable.cs b/Utils/src/VnDisposeable.cs new file mode 100644 index 0000000..4230a13 --- /dev/null +++ b/Utils/src/VnDisposeable.cs @@ -0,0 +1,85 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnDisposeable.cs +* +* VnDisposeable.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; +using System.Runtime.CompilerServices; + +namespace VNLib.Utils +{ + /// <summary> + /// Provides a base class with abstract methods for for disposable objects, with disposed check method + /// </summary> + public abstract class VnDisposeable : IDisposable + { + ///<inheritdoc/> + protected bool Disposed { get; private set; } + + /// <summary> + /// When overriden in a child class, is responsible for freeing resources + /// </summary> + protected abstract void Free(); + + /// <summary> + /// Checks if the current object has been disposed. Method will be inlined where possible + /// </summary> + /// <exception cref="ObjectDisposedException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual void Check() + { + if (Disposed) + { + throw new ObjectDisposedException("Object has been disposed"); + } + } + + /// <summary> + /// Sets the internal state to diposed without calling <see cref="Free"/> operation. + /// Usefull if another code-path performs the free operation independant of a dispose opreation. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void SetDisposedState() => Disposed = true; + ///<inheritdoc/> + protected virtual void Dispose(bool disposing) + { + if (!Disposed) + { + if (disposing) + { + //Call free method + Free(); + } + Disposed = true; + } + } + //Finalizer is not needed here + + ///<inheritdoc/> + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +}
\ No newline at end of file diff --git a/Utils/src/VnEncoding.cs b/Utils/src/VnEncoding.cs new file mode 100644 index 0000000..94d8a1a --- /dev/null +++ b/Utils/src/VnEncoding.cs @@ -0,0 +1,914 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnEncoding.cs +* +* VnEncoding.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; +using System.IO; +using System.Text; +using System.Buffers; +using System.Text.Json; +using System.Threading; +using System.Buffers.Text; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +using VNLib.Utils.IO; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + + +namespace VNLib.Utils +{ + /// <summary> + /// Contains static methods for encoding data + /// </summary> + public static class VnEncoding + { + /// <summary> + /// Encodes a <see cref="ReadOnlySpan{T}"/> with the specified <see cref="Encoding"/> to a <see cref="VnMemoryStream"/> that must be disposed by the user + /// </summary> + /// <param name="data">Data to be encoded</param> + /// <param name="encoding"><see cref="Encoding"/> to encode data with</param> + /// <returns>A <see cref="Stream"/> contating the encoded data</returns> + public static VnMemoryStream GetMemoryStream(in ReadOnlySpan<char> data, Encoding encoding) + { + _ = encoding ?? throw new ArgumentNullException(nameof(encoding)); + //Create new memory handle to copy data to + MemoryHandle<byte>? handle = null; + try + { + //get number of bytes + int byteCount = encoding.GetByteCount(data); + //resize the handle to fit the data + handle = Memory.Memory.Shared.Alloc<byte>(byteCount); + //encode + int size = encoding.GetBytes(data, handle); + //Consume the handle into a new vnmemstream and return it + return VnMemoryStream.ConsumeHandle(handle, size, true); + } + catch + { + //Dispose the handle if there is an excpetion + handle?.Dispose(); + throw; + } + } + + /// <summary> + /// Attempts to deserialze a json object from a stream of UTF8 data + /// </summary> + /// <typeparam name="T">The type of the object to deserialize</typeparam> + /// <param name="data">Binary data to read from</param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param> + /// <returns>The object decoded from the stream</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static T? JSONDeserializeFromBinary<T>(Stream? data, JsonSerializerOptions? options = null) + { + //Return default if null + if (data == null) + { + return default; + } + //Create a memory stream as a buffer + using VnMemoryStream ms = new(); + //Copy stream data to memory + data.CopyTo(ms, null); + if (ms.Length > 0) + { + //Rewind + ms.Position = 0; + //Recover data from stream + return ms.AsSpan().AsJsonObject<T>(options); + } + //Stream is empty + return default; + } + /// <summary> + /// Attempts to deserialze a json object from a stream of UTF8 data + /// </summary> + /// <param name="data">Binary data to read from</param> + /// <param name="type"></param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param> + /// <returns>The object decoded from the stream</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static object? JSONDeserializeFromBinary(Stream? data, Type type, JsonSerializerOptions? options = null) + { + //Return default if null + if (data == null) + { + return default; + } + //Create a memory stream as a buffer + using VnMemoryStream ms = new(); + //Copy stream data to memory + data.CopyTo(ms, null); + if (ms.Length > 0) + { + //Rewind + ms.Position = 0; + //Recover data from stream + return JsonSerializer.Deserialize(ms.AsSpan(), type, options); + } + //Stream is empty + return default; + } + /// <summary> + /// Attempts to deserialze a json object from a stream of UTF8 data + /// </summary> + /// <typeparam name="T">The type of the object to deserialize</typeparam> + /// <param name="data">Binary data to read from</param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param> + /// <param name="cancellationToken"></param> + /// <returns>The object decoded from the stream</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static ValueTask<T?> JSONDeserializeFromBinaryAsync<T>(Stream? data, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + //Return default if null + return data == null || data.Length == 0 ? ValueTask.FromResult<T?>(default) : JsonSerializer.DeserializeAsync<T>(data, options, cancellationToken); + } + /// <summary> + /// Attempts to deserialze a json object from a stream of UTF8 data + /// </summary> + /// <param name="data">Binary data to read from</param> + /// <param name="type"></param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param> + /// <param name="cancellationToken"></param> + /// <returns>The object decoded from the stream</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + public static ValueTask<object?> JSONDeserializeFromBinaryAsync(Stream? data, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + //Return default if null + return data == null || data.Length == 0 ? ValueTask.FromResult<object?>(default) : JsonSerializer.DeserializeAsync(data, type, options, cancellationToken); + } + /// <summary> + /// Attempts to serialize the object to json and write the encoded data to the stream + /// </summary> + /// <typeparam name="T">The object type to serialize</typeparam> + /// <param name="data">The object to serialize</param> + /// <param name="output">The <see cref="Stream"/> to write output data to</param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to serializer</param> + /// <exception cref="JsonException"></exception> + public static void JSONSerializeToBinary<T>(T data, Stream output, JsonSerializerOptions? options = null) + { + //return if null + if(data == null) + { + return; + } + //Serialize + JsonSerializer.Serialize(output, data, options); + } + /// <summary> + /// Attempts to serialize the object to json and write the encoded data to the stream + /// </summary> + /// <param name="data">The object to serialize</param> + /// <param name="output">The <see cref="Stream"/> to write output data to</param> + /// <param name="type"></param> + /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to serializer</param> + /// <exception cref="JsonException"></exception> + public static void JSONSerializeToBinary(object data, Stream output, Type type, JsonSerializerOptions? options = null) + { + //return if null + if (data == null) + { + return; + } + //Serialize + JsonSerializer.Serialize(output, data, type, options); + } + + #region Base32 + + private const string RFC_4648_BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + + /// <summary> + /// Attempts to convert the specified byte sequence in Base32 encoding + /// and writing the encoded data to the output buffer. + /// </summary> + /// <param name="input">The input buffer to convert</param> + /// <param name="output">The ouput buffer to write encoded data to</param> + /// <returns>The number of characters written, false if no data was written or output buffer was too small</returns> + public static ERRNO TryToBase32Chars(ReadOnlySpan<byte> input, Span<char> output) + { + ForwardOnlyWriter<char> writer = new(output); + return TryToBase32Chars(input, ref writer); + } + /// <summary> + /// Attempts to convert the specified byte sequence in Base32 encoding + /// and writing the encoded data to the output buffer. + /// </summary> + /// <param name="input">The input buffer to convert</param> + /// <param name="writer">A <see cref="ForwardOnlyWriter{T}"/> to write encoded chars to</param> + /// <returns>The number of characters written, false if no data was written or output buffer was too small</returns> + public static ERRNO TryToBase32Chars(ReadOnlySpan<byte> input, ref ForwardOnlyWriter<char> writer) + { + //calculate char size + int charCount = (int)Math.Ceiling(input.Length / 5d) * 8; + //Make sure there is enough room + if(charCount > writer.RemainingSize) + { + return false; + } + //sliding window over input buffer + ForwardOnlyReader<byte> reader = new(input); + + while (reader.WindowSize > 0) + { + //Convert the current window + WriteChars(reader.Window, ref writer); + //shift the window + reader.Advance(Math.Min(5, reader.WindowSize)); + } + return writer.Written; + } + private unsafe static void WriteChars(ReadOnlySpan<byte> input, ref ForwardOnlyWriter<char> writer) + { + //Get the input buffer as long + ulong inputAsLong = 0; + //Get a byte pointer over the ulong to index it as a byte buffer + byte* buffer = (byte*)&inputAsLong; + //Check proc endianness + if (BitConverter.IsLittleEndian) + { + //store each byte consecutivley and allow for padding + for (int i = 0; (i < 5 && i < input.Length); i++) + { + //Write bytes from upper to lower byte order for little endian systems + buffer[7 - i] = input[i]; + } + } + else + { + //store each byte consecutivley and allow for padding + for (int i = 0; (i < 5 && i < input.Length); i++) + { + //Write bytes from lower to upper byte order for Big Endian systems + buffer[i] = input[i]; + } + } + int rounds = (input.Length) switch + { + 1 => 2, + 2 => 4, + 3 => 5, + 4 => 7, + _ => 8 + }; + //Convert each byte segment up to the number of bytes encoded + for (int i = 0; i < rounds; i++) + { + //store the leading byte + byte val = buffer[7]; + //right shift the value to lower 5 bits + val >>= 3; + //Lookup charcode + char base32Char = RFC_4648_BASE32_CHARS[val]; + //append the character to the writer + writer.Append(base32Char); + //Shift input left by 5 bits so the next 5 bits can be read + inputAsLong <<= 5; + } + //Fill remaining bytes with padding chars + for(; rounds < 8; rounds++) + { + //Append trailing '=' + writer.Append('='); + } + } + + /// <summary> + /// Attempts to decode the Base32 encoded string + /// </summary> + /// <param name="input">The Base32 encoded data to decode</param> + /// <param name="output">The output buffer to write decoded data to</param> + /// <returns>The number of bytes written to the output</returns> + /// <exception cref="FormatException"></exception> + public static ERRNO TryFromBase32Chars(ReadOnlySpan<char> input, Span<byte> output) + { + ForwardOnlyWriter<byte> writer = new(output); + return TryFromBase32Chars(input, ref writer); + } + /// <summary> + /// Attempts to decode the Base32 encoded string + /// </summary> + /// <param name="input">The Base32 encoded data to decode</param> + /// <param name="writer">A <see cref="ForwardOnlyWriter{T}"/> to write decoded bytes to</param> + /// <returns>The number of bytes written to the output</returns> + /// <exception cref="FormatException"></exception> + public unsafe static ERRNO TryFromBase32Chars(ReadOnlySpan<char> input, ref ForwardOnlyWriter<byte> writer) + { + //TODO support Big-Endian byte order + + //trim padding characters + input = input.Trim('='); + //Calc the number of bytes to write + int outputSize = (input.Length * 5) / 8; + //make sure the output buffer is large enough + if(writer.RemainingSize < outputSize) + { + return false; + } + + //buffer used to shift data while decoding + ulong bufferLong = 0; + + //re-cast to byte* to index it as a byte buffer + byte* buffer = (byte*)&bufferLong; + + int count = 0, len = input.Length; + while(count < len) + { + //Convert the character to its char code + byte charCode = GetCharCode(input[count]); + //write byte to buffer + buffer[0] |= charCode; + count++; + //If 8 characters have been decoded, reset the buffer + if((count % 8) == 0) + { + //Write the 5 upper bytes in reverse order to the output buffer + for(int j = 0; j < 5; j++) + { + writer.Append(buffer[4 - j]); + } + //reset + bufferLong = 0; + } + //left shift the buffer up by 5 bits + bufferLong <<= 5; + } + //If remaining data has not be written, but has been buffed, finalize it + if (writer.Written < outputSize) + { + //calculate how many bits the buffer still needs to be shifted by (will be 5 bits off because of the previous loop) + int remainingShift = (7 - (count % 8)) * 5; + //right shift the buffer by the remaining bit count + bufferLong <<= remainingShift; + //calc remaining bytes + int remaining = (outputSize - writer.Written); + //Write remaining bytes to the output + for(int i = 0; i < remaining; i++) + { + writer.Append(buffer[4 - i]); + } + } + return writer.Written; + } + private static byte GetCharCode(char c) + { + //cast to byte to get its base 10 value + return c switch + { + //Upper case + 'A' => 0, + 'B' => 1, + 'C' => 2, + 'D' => 3, + 'E' => 4, + 'F' => 5, + 'G' => 6, + 'H' => 7, + 'I' => 8, + 'J' => 9, + 'K' => 10, + 'L' => 11, + 'M' => 12, + 'N' => 13, + 'O' => 14, + 'P' => 15, + 'Q' => 16, + 'R' => 17, + 'S' => 18, + 'T' => 19, + 'U' => 20, + 'V' => 21, + 'W' => 22, + 'X' => 23, + 'Y' => 24, + 'Z' => 25, + //Lower case + 'a' => 0, + 'b' => 1, + 'c' => 2, + 'd' => 3, + 'e' => 4, + 'f' => 5, + 'g' => 6, + 'h' => 7, + 'i' => 8, + 'j' => 9, + 'k' => 10, + 'l' => 11, + 'm' => 12, + 'n' => 13, + 'o' => 14, + 'p' => 15, + 'q' => 16, + 'r' => 17, + 's' => 18, + 't' => 19, + 'u' => 20, + 'v' => 21, + 'w' => 22, + 'x' => 23, + 'y' => 24, + 'z' => 25, + //Base10 digits + '2' => 26, + '3' => 27, + '4' => 28, + '5' => 29, + '6' => 30, + '7' => 31, + + _=> throw new FormatException("Character found is not a Base32 encoded character") + }; + } + + /// <summary> + /// Calculates the maximum buffer size required to encode a binary block to its Base32 + /// character encoding + /// </summary> + /// <param name="bufferSize">The binary buffer size used to calculate the base32 buffer size</param> + /// <returns>The maximum size (including padding) of the character buffer required to encode the binary data</returns> + public static int Base32CalcMaxBufferSize(int bufferSize) + { + /* + * Base32 encoding consumes 8 bytes for every 5 bytes + * of input data + */ + //Add up to 8 bytes for padding + return (int)(Math.Ceiling(bufferSize / 5d) * 8) + (8 - (bufferSize % 8)); + } + + /// <summary> + /// Converts the binary buffer to a base32 character string with optional padding characters + /// </summary> + /// <param name="binBuffer">The buffer to encode</param> + /// <param name="withPadding">Should padding be included in the result</param> + /// <returns>The base32 encoded string representation of the specified buffer</returns> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static string ToBase32String(ReadOnlySpan<byte> binBuffer, bool withPadding = false) + { + string value; + //Calculate the base32 entropy to alloc an appropriate buffer (minium buffer of 2 chars) + int entropy = Base32CalcMaxBufferSize(binBuffer.Length); + //Alloc buffer for enough size (2*long bytes) is not an issue + using (UnsafeMemoryHandle<char> charBuffer = Memory.Memory.UnsafeAlloc<char>(entropy)) + { + //Encode + ERRNO encoded = TryToBase32Chars(binBuffer, charBuffer.Span); + if (!encoded) + { + throw new InternalBufferTooSmallException("Base32 char buffer was too small"); + } + //Convert with or w/o padding + if (withPadding) + { + value = charBuffer.Span[0..(int)encoded].ToString(); + } + else + { + value = charBuffer.Span[0..(int)encoded].Trim('=').ToString(); + } + } + return value; + } + /// <summary> + /// Converts the base32 character buffer to its structure representation + /// </summary> + /// <typeparam name="T">The structure type</typeparam> + /// <param name="base32">The base32 character buffer</param> + /// <returns>The new structure of the base32 data</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static T FromBase32String<T>(ReadOnlySpan<char> base32) where T: unmanaged + { + //calc size of bin buffer + int size = base32.Length; + //Rent a bin buffer + using UnsafeMemoryHandle<byte> binBuffer = Memory.Memory.UnsafeAlloc<byte>(size); + //Try to decode the data + ERRNO decoded = TryFromBase32Chars(base32, binBuffer.Span); + //Marshal back to a struct + return decoded ? MemoryMarshal.Read<T>(binBuffer.Span[..(int)decoded]) : throw new InternalBufferTooSmallException("Binbuffer was too small"); + } + + /// <summary> + /// Gets a byte array of the base32 decoded data + /// </summary> + /// <param name="base32">The character array to decode</param> + /// <returns>The byte[] of the decoded binary data, or null if the supplied character array was empty</returns> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static byte[]? FromBase32String(ReadOnlySpan<char> base32) + { + if (base32.IsEmpty) + { + return null; + } + //Buffer size of the base32 string will always be enough buffer space + using UnsafeMemoryHandle<byte> tempBuffer = Memory.Memory.UnsafeAlloc<byte>(base32.Length); + //Try to decode the data + ERRNO decoded = TryFromBase32Chars(base32, tempBuffer.Span); + + return decoded ? tempBuffer.Span[0..(int)decoded].ToArray() : throw new InternalBufferTooSmallException("Binbuffer was too small"); + } + + /// <summary> + /// Converts a structure to its base32 representation and returns the string of its value + /// </summary> + /// <typeparam name="T">The structure type</typeparam> + /// <param name="value">The structure to encode</param> + /// <param name="withPadding">A value indicating if padding should be used</param> + /// <returns>The base32 string representation of the structure</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static string ToBase32String<T>(T value, bool withPadding = false) where T : unmanaged + { + //get the size of the structure + int binSize = Unsafe.SizeOf<T>(); + //Rent a bin buffer + Span<byte> binBuffer = stackalloc byte[binSize]; + //Write memory to buffer + MemoryMarshal.Write(binBuffer, ref value); + //Convert to base32 + return ToBase32String(binBuffer, withPadding); + } + + #endregion + + #region percent encoding + + private static readonly ReadOnlyMemory<byte> HexToUtf8Pos = new byte[16] + { + 0x30, //0 + 0x31, //1 + 0x32, //2 + 0x33, //3 + 0x34, //4 + 0x35, //5 + 0x36, //6 + 0x37, //7 + 0x38, //8 + 0x39, //9 + + 0x41, //A + 0x42, //B + 0x43, //C + 0x44, //D + 0x45, //E + 0x46 //F + }; + + /// <summary> + /// Deterimes the size of the buffer needed to encode a utf8 encoded + /// character buffer into its url-safe percent/hex encoded representation + /// </summary> + /// <param name="utf8Bytes">The buffer to examine</param> + /// <param name="allowedChars">A sequence of characters that are excluded from encoding</param> + /// <returns>The size of the buffer required to encode</returns> + public static unsafe int PercentEncodeCalcBufferSize(ReadOnlySpan<byte> utf8Bytes, in ReadOnlySpan<byte> allowedChars = default) + { + /* + * For every illegal character, the percent encoding adds 3 bytes of + * entropy. So a single byte will be replaced by 3, so adding + * 2 bytes for every illegal character plus the length of the + * intial buffer, we get the size of the buffer needed to + * percent encode. + */ + int count = 0, len = utf8Bytes.Length; + fixed (byte* utfBase = &MemoryMarshal.GetReference(utf8Bytes)) + { + //Find all unsafe characters and add the entropy size + for (int i = 0; i < len; i++) + { + if (!IsUrlSafeChar(utfBase[i], in allowedChars)) + { + count += 2; + } + } + } + //Size is initial buffer size + count bytes + return len + count; + } + + /// <summary> + /// Percent encodes the buffer for utf8 encoded characters to its percent/hex encoded + /// utf8 character representation + /// </summary> + /// <param name="utf8Bytes">The buffer of utf8 encoded characters to encode</param> + /// <param name="utf8Output">The buffer to write the encoded characters to</param> + /// <param name="allowedChars">A sequence of characters that are excluded from encoding</param> + /// <returns>The number of characters encoded and written to the output buffer</returns> + public static ERRNO PercentEncode(ReadOnlySpan<byte> utf8Bytes, Span<byte> utf8Output, in ReadOnlySpan<byte> allowedChars = default) + { + int outPos = 0, len = utf8Bytes.Length; + ReadOnlySpan<byte> lookupTable = HexToUtf8Pos.Span; + for (int i = 0; i < len; i++) + { + byte value = utf8Bytes[i]; + //Check if value is url safe + if(IsUrlSafeChar(value, in allowedChars)) + { + //Skip + utf8Output[outPos++] = value; + } + else + { + //Percent encode + utf8Output[outPos++] = 0x25; // '%' + //Calc and store the encoded by the upper 4 bits + utf8Output[outPos++] = lookupTable[(value & 0xf0) >> 4]; + //Store lower 4 bits in encoded value + utf8Output[outPos++] = lookupTable[value & 0x0f]; + } + } + //Return the size of the output buffer + return outPos; + } + + private static bool IsUrlSafeChar(byte value, in ReadOnlySpan<byte> allowedChars) + { + return + // base10 digits + value > 0x2f && value < 0x3a + // '_' (underscore) + || value == 0x5f + // '-' (hyphen) + || value == 0x2d + // Uppercase letters + || value > 0x40 && value < 0x5b + // lowercase letters + || value > 0x60 && value < 0x7b + // Check allowed characters + || allowedChars.Contains(value); + } + + //TODO: Implement decode with better performance, lookup table or math vs searching the table + + /// <summary> + /// Decodes a percent (url/hex) encoded utf8 encoded character buffer to its utf8 + /// encoded binary value + /// </summary> + /// <param name="utf8Encoded">The buffer containg characters to be decoded</param> + /// <param name="utf8Output">The buffer to write deocded values to</param> + /// <returns>The nuber of bytes written to the output buffer</returns> + /// <exception cref="FormatException"></exception> + public static ERRNO PercentDecode(ReadOnlySpan<byte> utf8Encoded, Span<byte> utf8Output) + { + int outPos = 0, len = utf8Encoded.Length; + ReadOnlySpan<byte> lookupTable = HexToUtf8Pos.Span; + for (int i = 0; i < len; i++) + { + byte value = utf8Encoded[i]; + //Begining of percent encoding character + if(value == 0x25) + { + //Calculate the base16 multiplier from the upper half of the + int multiplier = lookupTable.IndexOf(utf8Encoded[i + 1]); + //get the base16 lower half to add + int lower = lookupTable.IndexOf(utf8Encoded[i + 2]); + //Check format + if(multiplier < 0 || lower < 0) + { + throw new FormatException($"Encoded buffer contains invalid hexadecimal characters following the % character at position {i}"); + } + //Calculate the new value, shift multiplier to the upper 4 bits, then mask + or the lower 4 bits + value = (byte)(((byte)(multiplier << 4)) | ((byte)lower & 0x0f)); + //Advance the encoded index by the two consumed chars + i += 2; + } + utf8Output[outPos++] = value; + } + return outPos; + } + + #endregion + + #region Base64 + + /// <summary> + /// Tries to convert the specified span containing a string representation that is + /// encoded with base-64 digits into a span of 8-bit unsigned integers. + /// </summary> + /// <param name="base64">Base64 character data to recover</param> + /// <param name="buffer">The binary output buffer to write converted characters to</param> + /// <returns>The number of bytes written, or <see cref="ERRNO.E_FAIL"/> of the conversion was unsucessful</returns> + public static ERRNO TryFromBase64Chars(ReadOnlySpan<char> base64, Span<byte> buffer) + { + return Convert.TryFromBase64Chars(base64, buffer, out int bytesWritten) ? bytesWritten : ERRNO.E_FAIL; + } + /// <summary> + /// Tries to convert the 8-bit unsigned integers inside the specified read-only span + /// into their equivalent string representation that is encoded with base-64 digits. + /// You can optionally specify whether to insert line breaks in the return value. + /// </summary> + /// <param name="buffer">The binary buffer to convert characters from</param> + /// <param name="base64">The base64 output buffer</param> + /// <param name="options"> + /// One of the enumeration values that specify whether to insert line breaks in the + /// return value. The default value is System.Base64FormattingOptions.None. + /// </param> + /// <returns>The number of characters encoded, or <see cref="ERRNO.E_FAIL"/> if conversion was unsuccessful</returns> + public static ERRNO TryToBase64Chars(ReadOnlySpan<byte> buffer, Span<char> base64, Base64FormattingOptions options = Base64FormattingOptions.None) + { + return Convert.TryToBase64Chars(buffer, base64, out int charsWritten, options: options) ? charsWritten : ERRNO.E_FAIL; + } + + + /* + * Calc base64 padding chars excluding the length mod 4 = 0 case + * by and-ing 0x03 (011) with the result + */ + + /// <summary> + /// Determines the number of missing padding bytes from the length of the base64 + /// data sequence. + /// <code> + /// Formula (4 - (length mod 4) and 0x03 + /// </code> + /// </summary> + /// <param name="length">The length of the base64 buffer</param> + /// <returns>The number of padding bytes to add to the end of the sequence</returns> + public static int Base64CalcRequiredPadding(int length) => (4 - (length % 4)) & 0x03; + + /// <summary> + /// Converts a base64 utf8 encoded binary buffer to + /// its base64url encoded version + /// </summary> + /// <param name="base64">The binary buffer to convert</param> + public static unsafe void Base64ToUrlSafeInPlace(Span<byte> base64) + { + int len = base64.Length; + + fixed(byte* ptr = &MemoryMarshal.GetReference(base64)) + { + for (int i = 0; i < len; i++) + { + switch (ptr[i]) + { + //Replace + with - (minus) + case 0x2b: + ptr[i] = 0x2d; + break; + //Replace / with _ (underscore) + case 0x2f: + ptr[i] = 0x5f; + break; + } + } + } + } + /// <summary> + /// Converts a base64url encoded utf8 encoded binary buffer to + /// its base64 encoded version + /// </summary> + /// <param name="uft8Base64Url">The base64url utf8 to decode</param> + public static unsafe void Base64FromUrlSafeInPlace(Span<byte> uft8Base64Url) + { + int len = uft8Base64Url.Length; + + fixed (byte* ptr = &MemoryMarshal.GetReference(uft8Base64Url)) + { + for (int i = 0; i < len; i++) + { + switch (ptr[i]) + { + //Replace - with + (plus) + case 0x2d: + ptr[i] = 0x2b; + break; + //Replace _ with / (slash) + case 0x5f: + ptr[i] = 0x2f; + break; + } + } + } + } + + /// <summary> + /// Converts the input buffer to a url safe base64 encoded + /// utf8 buffer from the base64 input buffer. The base64 is copied + /// directly to the output then converted in place. This is + /// just a shortcut method for readonly spans + /// </summary> + /// <param name="base64">The base64 encoded data</param> + /// <param name="base64Url">The base64url encoded output</param> + /// <returns>The size of the <paramref name="base64"/> buffer</returns> + public static ERRNO Base64ToUrlSafe(ReadOnlySpan<byte> base64, Span<byte> base64Url) + { + //Aligned copy to the output buffer + base64.CopyTo(base64Url); + //One time convert the output buffer to url safe + Base64ToUrlSafeInPlace(base64Url); + return base64.Length; + } + + /// <summary> + /// Converts the urlsafe input buffer to a base64 encoded + /// utf8 buffer from the base64 input buffer. The base64 is copied + /// directly to the output then converted in place. This is + /// just a shortcut method for readonly spans + /// </summary> + /// <param name="base64">The base64 encoded data</param> + /// <param name="base64Url">The base64url encoded output</param> + /// <returns>The size of the <paramref name="base64Url"/> buffer</returns> + public static ERRNO Base64FromUrlSafe(ReadOnlySpan<byte> base64Url, Span<byte> base64) + { + //Aligned copy to the output buffer + base64Url.CopyTo(base64); + //One time convert the output buffer to url safe + Base64FromUrlSafeInPlace(base64); + return base64Url.Length; + } + + /// <summary> + /// Decodes a utf8 base64url encoded sequence of data and writes it + /// to the supplied output buffer + /// </summary> + /// <param name="utf8Base64Url">The utf8 base64 url encoded string</param> + /// <param name="output">The output buffer to write the decoded data to</param> + /// <returns>The number of bytes written or <see cref="ERRNO.E_FAIL"/> if the operation failed</returns> + public static ERRNO Base64UrlDecode(ReadOnlySpan<byte> utf8Base64Url, Span<byte> output) + { + if(utf8Base64Url.IsEmpty || output.IsEmpty) + { + return ERRNO.E_FAIL; + } + //url deocde + ERRNO count = Base64FromUrlSafe(utf8Base64Url, output); + + //Writer for adding padding bytes + ForwardOnlyWriter<byte> writer = new (output); + writer.Advance(count); + + //Calc required padding + int paddingToAdd = Base64CalcRequiredPadding(writer.Written); + //Add padding bytes + for (; paddingToAdd > 0; paddingToAdd--) + { + writer.Append(0x3d); // '=' + } + + //Base64 decode in place, we should have a buffer large enough + OperationStatus status = Base64.DecodeFromUtf8InPlace(writer.AsSpan(), out int bytesWritten); + //If status is successful return the number of bytes written + return status == OperationStatus.Done ? bytesWritten : ERRNO.E_FAIL; + } + + /// <summary> + /// Decodes a base64url encoded character sequence + /// of data and writes it to the supplied output buffer + /// </summary> + /// <param name="chars">The character buffer to decode</param> + /// <param name="output">The output buffer to write decoded data to</param> + /// <param name="encoding">The character encoding</param> + /// <returns>The number of bytes written or <see cref="ERRNO.E_FAIL"/> if the operation failed</returns> + /// <exception cref="InternalBufferTooSmallException"></exception> + public static ERRNO Base64UrlDecode(ReadOnlySpan<char> chars, Span<byte> output, Encoding? encoding = null) + { + if (chars.IsEmpty || output.IsEmpty) + { + return ERRNO.E_FAIL; + } + //Set the encoding to utf8 + encoding ??= Encoding.UTF8; + //get the number of bytes to alloc a buffer + int decodedSize = encoding.GetByteCount(chars); + + //alloc buffer + using UnsafeMemoryHandle<byte> decodeHandle = Memory.Memory.UnsafeAlloc<byte>(decodedSize); + //Get the utf8 binary data + int count = encoding.GetBytes(chars, decodeHandle); + return Base64UrlDecode(decodeHandle.Span[..count], output); + } + + #endregion + } +}
\ No newline at end of file |