diff options
author | vnugent <public@vaughnnugent.com> | 2023-01-08 14:44:01 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-01-08 14:44:01 -0500 |
commit | be6dc557a3b819248b014992eb96c1cb21f8112b (patch) | |
tree | 5361530552856ba8154bfcfbfac8377549117c9e /Utils/src/Extensions | |
parent | 072a1294646542a73007784d08a35ffcad557b1b (diff) |
Initial commit
Diffstat (limited to 'Utils/src/Extensions')
-rw-r--r-- | Utils/src/Extensions/CacheExtensions.cs | 348 | ||||
-rw-r--r-- | Utils/src/Extensions/CollectionExtensions.cs | 98 | ||||
-rw-r--r-- | Utils/src/Extensions/IoExtensions.cs | 345 | ||||
-rw-r--r-- | Utils/src/Extensions/JsonExtensions.cs | 215 | ||||
-rw-r--r-- | Utils/src/Extensions/MemoryExtensions.cs | 769 | ||||
-rw-r--r-- | Utils/src/Extensions/MutexReleaser.cs | 62 | ||||
-rw-r--r-- | Utils/src/Extensions/SafeLibraryExtensions.cs | 103 | ||||
-rw-r--r-- | Utils/src/Extensions/SemSlimReleaser.cs | 51 | ||||
-rw-r--r-- | Utils/src/Extensions/StringExtensions.cs | 481 | ||||
-rw-r--r-- | Utils/src/Extensions/ThreadingExtensions.cs | 226 | ||||
-rw-r--r-- | Utils/src/Extensions/TimerExtensions.cs | 66 | ||||
-rw-r--r-- | Utils/src/Extensions/VnStringExtensions.cs | 418 |
12 files changed, 3182 insertions, 0 deletions
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 |