From be6dc557a3b819248b014992eb96c1cb21f8112b Mon Sep 17 00:00:00 2001 From: vnugent Date: Sun, 8 Jan 2023 14:44:01 -0500 Subject: Initial commit --- Utils/src/Extensions/CacheExtensions.cs | 348 ++++++++++++ Utils/src/Extensions/CollectionExtensions.cs | 98 ++++ Utils/src/Extensions/IoExtensions.cs | 345 ++++++++++++ Utils/src/Extensions/JsonExtensions.cs | 215 +++++++ Utils/src/Extensions/MemoryExtensions.cs | 769 ++++++++++++++++++++++++++ Utils/src/Extensions/MutexReleaser.cs | 62 +++ Utils/src/Extensions/SafeLibraryExtensions.cs | 103 ++++ Utils/src/Extensions/SemSlimReleaser.cs | 51 ++ Utils/src/Extensions/StringExtensions.cs | 481 ++++++++++++++++ Utils/src/Extensions/ThreadingExtensions.cs | 226 ++++++++ Utils/src/Extensions/TimerExtensions.cs | 66 +++ Utils/src/Extensions/VnStringExtensions.cs | 418 ++++++++++++++ 12 files changed, 3182 insertions(+) create mode 100644 Utils/src/Extensions/CacheExtensions.cs create mode 100644 Utils/src/Extensions/CollectionExtensions.cs create mode 100644 Utils/src/Extensions/IoExtensions.cs create mode 100644 Utils/src/Extensions/JsonExtensions.cs create mode 100644 Utils/src/Extensions/MemoryExtensions.cs create mode 100644 Utils/src/Extensions/MutexReleaser.cs create mode 100644 Utils/src/Extensions/SafeLibraryExtensions.cs create mode 100644 Utils/src/Extensions/SemSlimReleaser.cs create mode 100644 Utils/src/Extensions/StringExtensions.cs create mode 100644 Utils/src/Extensions/ThreadingExtensions.cs create mode 100644 Utils/src/Extensions/TimerExtensions.cs create mode 100644 Utils/src/Extensions/VnStringExtensions.cs (limited to 'Utils/src/Extensions') 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 +{ + /// + /// Cache collection extensions + /// + public static class CacheExtensions + { + /// + /// + /// 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 + /// + /// + /// + /// A cachable object + /// + /// The unique key identifying the record + /// The record to store + /// + /// Locks on the store parameter to provide mutual exclusion for non thread-safe + /// data structures. + /// + public static void StoreRecord(this IDictionary 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(); + } + /// + /// + /// 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 + /// + /// + /// + /// A cachable object + /// + /// The unique key identifying the record + /// The record to store + /// The new expiration time of the record + /// + /// Locks on the store parameter to provide mutual exclusion for non thread-safe + /// data structures. + /// + public static void StoreRecord(this IDictionary 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); + } + /// + /// + /// Returns a stored record if it exists and is not expired. If the record exists + /// but has expired, it is evicted. + /// + /// + /// 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 + /// + /// + /// + /// A cachable object + /// + /// + /// The record + /// + /// Gets a value indicating the reults of the operation. 0 if the record is not found, -1 if expired, 1 if + /// record is valid + /// + /// + /// Locks on the store parameter to provide mutual exclusion for non thread-safe + /// data structures. + /// + public static ERRNO TryGetOrEvictRecord(this IDictionary 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; + } + /// + /// Updates the expiration date on a record to the specified time if it exists, regardless + /// of its validity + /// + /// Diction key type + /// A cachable object + /// + /// The unique key identifying the record to update + /// The expiration time (time added to ) + /// + /// Locks on the store parameter to provide mutual exclusion for non thread-safe + /// data structures. + /// + public static void UpdateRecord(this IDictionary 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; + } + } + } + /// + /// Evicts a stored record from the store. If the record is found, the eviction + /// method is executed + /// + /// + /// + /// + /// The unique key identifying the record + /// True if the record was found and evicted + public static bool EvictRecord(this IDictionary 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; + } + /// + /// Evicts all expired records from the store + /// + /// + /// + public static void CollectRecords(this IDictionary store) where T : ICacheable + { + CollectRecords(store, DateTime.UtcNow); + } + + /// + /// Evicts all expired records from the store + /// + /// + /// + /// + /// A time that specifies the time which expired records should be evicted + public static void CollectRecords(this IDictionary store, DateTime validAfter) where T : ICacheable + { + //Build a query to get the keys that belong to the expired records + IEnumerable> expired = store.Where(s => s.Value.Expires < validAfter); + //temp list for expired records + IEnumerable evicted; + //Take lock on store + lock (store) + { + KeyValuePair[] kvp = expired.ToArray(); + //enumerate to array so values can be removed while the lock is being held + foreach (KeyValuePair 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(); + } + } + + /// + /// Allows for mutually exclusive use of a record with a + /// state parameter + /// + /// + /// + /// + /// + /// The unique key identifying the record + /// A user-token type state parameter to pass to the use callback method + /// A callback method that will be passed the record to use within an exclusive context + public static void UseRecord(this IDictionary store, TKey key, State state, Action 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); + } + } + } + /// + /// Allows for mutually exclusive use of a + /// + /// + /// + /// + /// The unique key identifying the record + /// A callback method that will be passed the record to use within an exclusive context + public static void UseRecord(this IDictionary store, TKey key, Action 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); + } + } + } + /// + /// Allows for mutually exclusive use of a record with a + /// state parameter, only if the found record is valid + /// + /// + /// + /// + /// + /// The unique key identifying the record + /// A user-token type state parameter to pass to the use callback method + /// A callback method that will be passed the record to use within an exclusive context + /// If the record is found, but is expired, the record is evicted from the store. The callback is never invoked + public static void UseIfValid(this IDictionary store, TKey key, State state, Action 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(); + } + /// + /// Allows for mutually exclusive use of a record with a + /// state parameter, only if the found record is valid + /// + /// + /// + /// + /// The unique key identifying the record + /// A callback method that will be passed the record to use within an exclusive context + /// If the record is found, but is expired, the record is evicted from the store. The callback is never invoked + public static void UseIfValid(this IDictionary store, TKey key, Action 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 +{ + /// + /// Provides collection extension methods + /// + public static class CollectionExtensions + { + /// + /// Gets a previously-stored base32 encoded value-type from the lookup and returns its initialized structure from + /// the value stored + /// + /// The key type used to index the lookup + /// An unmanaged structure type + /// + /// The key used to identify the value + /// The initialized structure, or default if the lookup returns null/empty string + public static TValue GetValueType(this IIndexable 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(value); + } + + /// + /// Serializes a value-type in base32 encoding and stores it at the specified key + /// + /// The key type used to index the lookup + /// An unmanaged structure type + /// + /// The key used to identify the value + /// The value to serialze + public static void SetValueType(this IIndexable 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); + } + /// + /// Executes a handler delegate on every element of the list within a try-catch block + /// and rethrows exceptions as an + /// + /// + /// + /// An handler delegate to complete some operation on the elements within the list + /// + public static void TryForeach(this IEnumerable list, Action handler) + { + List? 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 +{ + /// + /// Provieds extension methods for common IO operations + /// + public static class IoExtensions + { + /// + /// Unlocks the entire file + /// + [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); + } + + /// + /// Locks the entire file + /// + [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); + } + + /// + /// Provides an async wrapper for copying data from the current stream to another using an unmanged + /// buffer. + /// + /// + /// The destination data stream to write data to + /// The size of the buffer to use while copying data. (Value will be clamped to the size of the stream if seeking is available) + /// The to allocate the buffer from + /// A token that may cancel asynchronous operations + /// A that completes when the copy operation has completed + /// + /// + 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 buffer = heap.DirectAlloc(bufferSize); + //Wait for copy to complete + await CopyToAsync(source, dest, buffer.Memory, token); + } + /// + /// Provides an async wrapper for copying data from the current stream to another with a + /// buffer from the + /// + /// + /// The destination data stream to write data to + /// The size of the buffer to use while copying data. (Value will be clamped to the size of the stream if seeking is available) + /// The number of bytes to copy from the current stream to destination stream + /// The heap to alloc buffer from + /// A token that may cancel asynchronous operations + /// A that completes when the copy operation has completed + /// + /// + 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 buffer = heap.DirectAlloc(bufferSize); + //Wait for copy to complete + await CopyToAsync(source, dest, buffer.Memory, count, token); + } + + /// + /// Copies data from one stream to another, using self managed buffers. May allocate up to 2MB. + /// + /// Source stream to read from + /// Destination stream to write data to + /// The heap to allocate buffers from + /// + /// + 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 buffer = heap.UnsafeAlloc(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); + } + /// + /// Copies data from one stream to another, using self managed buffers. May allocate up to 2MB. + /// + /// Source stream to read from + /// Destination stream to write data to + /// Number of bytes to read/write + /// The heap to allocate buffers from + /// + /// + 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 buffer = heap.UnsafeAlloc(bufSize); + //wrapper around offset pointer + long total = 0; + int read; + do + { + Span 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); + } + + /// + /// Copies data from the current stream to the destination stream using the supplied memory buffer + /// + /// + /// The destination data stream to write data to + /// The buffer to use when copying data + /// A token that may cancel asynchronous operations + /// A that completes when the copy operation has completed + /// + public static async ValueTask CopyToAsync(this Stream source, Stream dest, Memory 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); + } + } + + /// + /// Copies data from the current stream to the destination stream using the supplied memory buffer + /// + /// + /// The destination data stream to write data to + /// The buffer to use when copying data + /// The number of bytes to copy from the current stream to destination stream + /// A token that may cancel asynchronous operations + /// A that completes when the copy operation has completed + /// + public static async ValueTask CopyToAsync(this Stream source, Stream dest, Memory 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 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; + } + } + + /// + /// Opens a file within the current directory + /// + /// + /// The name of the file to open + /// The to open the file with + /// The to open the file with + /// + /// The size of the buffer to read/write with + /// + /// The of the opened file + [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); + } + /// + /// Deletes the speicifed file from the current directory + /// + /// + /// The name of the file to delete + [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); + } + /// + /// Determines if a file exists within the current directory + /// + /// + /// The name of the file to search for + /// True if the file is found and the user has permission to access the file, false otherwise + [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 +{ + /// + /// Specifies how to parse a timespan value from a element + /// + public enum TimeParseType + { + Milliseconds, + Seconds, + Minutes, + Hours, + Days, + Ticks + } + + public static class JsonExtensions + { + /// + /// Converts a JSON encoded string to an object of the specified type + /// + /// Output type of the object + /// + /// to use during de-serialization + /// The new object or default if the string is null or empty + /// + /// + public static T? AsJsonObject(this string value, JsonSerializerOptions? options = null) + { + return !string.IsNullOrWhiteSpace(value) ? JsonSerializer.Deserialize(value, options) : default; + } + /// + /// Converts a JSON encoded binary data to an object of the specified type + /// + /// Output type of the object + /// + /// to use during de-serialization + /// The new object or default if the string is null or empty + /// + /// + public static T? AsJsonObject(this in ReadOnlySpan utf8bin, JsonSerializerOptions? options = null) + { + return utf8bin.IsEmpty ? default : JsonSerializer.Deserialize(utf8bin, options); + } + /// + /// Converts a JSON encoded binary data to an object of the specified type + /// + /// Output type of the object + /// + /// to use during de-serialization + /// The new object or default if the string is null or empty + /// + /// + public static T? AsJsonObject(this in ReadOnlyMemory utf8bin, JsonSerializerOptions? options = null) + { + return utf8bin.IsEmpty ? default : JsonSerializer.Deserialize(utf8bin.Span, options); + } + /// + /// Converts a JSON encoded binary data to an object of the specified type + /// + /// Output type of the object + /// + /// to use during de-serialization + /// The new object or default if the string is null or empty + /// + /// + public static T? AsJsonObject(this byte[] utf8bin, JsonSerializerOptions? options = null) + { + return utf8bin == null ? default : JsonSerializer.Deserialize(utf8bin.AsSpan(), options); + } + /// + /// Parses a json encoded string to a json documen + /// + /// + /// + /// If the json string is null, returns null, otherwise the json document around the data + /// + public static JsonDocument? AsJsonDocument(this string jsonString, JsonDocumentOptions options = default) + { + return jsonString == null ? null : JsonDocument.Parse(jsonString, options); + } + /// + /// Shortcut extension to and returns a string + /// + /// + /// The name of the property to get the string value of + /// If the property exists, returns the string stored at that property + public static string? GetPropString(this in JsonElement element, string propertyName) + { + return element.TryGetProperty(propertyName, out JsonElement el) ? el.GetString() : null; + } + /// + /// Shortcut extension to and returns a string + /// + /// + /// The name of the property to get the string value of + /// If the property exists, returns the string stored at that property + public static string? GetPropString(this IReadOnlyDictionary conf, string propertyName) + { + return conf.TryGetValue(propertyName, out JsonElement el) ? el.GetString() : null; + } + + /// + /// Shortcut extension to and returns a string + /// + /// + /// The name of the property to get the string value of + /// If the property exists, returns the string stored at that property + public static string? GetPropString(this IDictionary conf, string propertyName) + { + return conf.TryGetValue(propertyName, out JsonElement el) ? el.GetString() : null; + } + + /// + /// Attemts to serialze an object to a JSON encoded string + /// + /// + /// to use during serialization + /// A JSON encoded string of the serialized object, or null if the object is null + /// + public static string? ToJsonString(this T obj, JsonSerializerOptions? options = null) + { + return obj == null ? null : JsonSerializer.Serialize(obj, options); + } + + /// + /// Merges the current with another to + /// create a new document of combined properties + /// + /// + /// The to combine with the first document + /// The name of the new element containing the initial document data + /// The name of the new element containing the additional document data + /// A new document with a parent root containing the combined objects + 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); + } + + /// + /// Parses a number value into a of the specified time + /// + /// + /// The the value represents + /// The of the value + /// + /// + /// + /// + /// + 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; + + /// + /// Provides memory based extensions to .NET and VNLib memory abstractions + /// + public static class MemoryExtensions + { + /// + /// Rents a new array and stores it as a resource within an to return the + /// array when work is completed + /// + /// + /// + /// The minimum size array to allocate + /// Should elements from 0 to size be set to default(T) + /// A new encapsulating the rented array + public static UnsafeMemoryHandle Lease(this ArrayPool 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); + } + + /// + /// Retreives a buffer that is at least the reqested length, and clears the array from 0-size. + ///

+ /// The array may be larger than the requested size, and the entire buffer is zeroed + ///
+ /// + /// The minimum length of the array + /// True if contents should be zeroed + /// The zeroed array + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T[] Rent(this ArrayPool 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; + } + + /// + /// Copies the characters within the memory handle to a + /// + /// The string representation of the buffer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ToString(this T charBuffer) where T: IMemoryHandle + { + return charBuffer.Span.ToString(); + } + + /// + /// Wraps the instance in System.Buffers.MemoryManager + /// wrapper to provide buffers from umanaged handles. + /// + /// The unmanaged data type + /// + /// + /// A value that indicates if the new owns the handle. + /// When true, the new maintains the lifetime of the handle. + /// + /// The wrapper + /// NOTE: This wrapper now manages the lifetime of the current handle + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryManager ToMemoryManager(this MemoryHandle handle, bool ownsHandle = true) where T : unmanaged + { + _ = handle ?? throw new ArgumentNullException(nameof(handle)); + return new SysBufferMemoryManager(handle, ownsHandle); + } + + /// + /// Wraps the instance in System.Buffers.MemoryManager + /// wrapper to provide buffers from umanaged handles. + /// + /// The unmanaged data type + /// + /// + /// A value that indicates if the new owns the handle. + /// When true, the new maintains the lifetime of the handle. + /// + /// The wrapper + /// NOTE: This wrapper now manages the lifetime of the current handle + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryManager ToMemoryManager(this VnTempBuffer handle, bool ownsHandle = true) where T : unmanaged + { + _ = handle ?? throw new ArgumentNullException(nameof(handle)); + return new SysBufferMemoryManager(handle, ownsHandle); + } + + /// + /// Allows direct allocation of a fixed size from a instance + /// of the specified number of elements + /// + /// The unmanaged data type + /// + /// The number of elements to allocate on the heap + /// Optionally zeros conents of the block when allocated + /// The wrapper around the block of memory + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryManager DirectAlloc(this IUnmangedHeap heap, ulong size, bool zero = false) where T : unmanaged + { + return new SysBufferMemoryManager(heap, size, zero); + } + + /// + /// Allows direct allocation of a fixed size from a instance + /// of the specified number of elements + /// + /// The unmanaged data type + /// + /// The number of elements to allocate on the heap + /// Optionally zeros conents of the block when allocated + /// The wrapper around the block of memory + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryManager DirectAlloc(this IUnmangedHeap heap, long size, bool zero = false) where T : unmanaged + { + return size < 0 ? throw new ArgumentOutOfRangeException(nameof(size)) : DirectAlloc(heap, (ulong)size, zero); + } + /// + /// Gets an offset pointer from the base postion to the number of bytes specified. Performs bounds checks + /// + /// + /// Number of elements of type to offset + /// + /// + /// pointer to the memory offset specified + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe T* GetOffset(this MemoryHandle memory, long elements) where T : unmanaged + { + return elements < 0 ? throw new ArgumentOutOfRangeException(nameof(elements)) : memory.GetOffset((ulong)elements); + } + /// + /// Resizes the current handle on the heap + /// + /// + /// Positive number of elemnts the current handle should referrence + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Resize(this MemoryHandle memory, long elements) where T : unmanaged + { + if (elements < 0) + { + throw new ArgumentOutOfRangeException(nameof(elements)); + } + memory.Resize((ulong)elements); + } + + /// + /// Resizes the target handle only if the handle is smaller than the requested element count + /// + /// + /// + /// The number of elements to resize to + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ResizeIfSmaller(this MemoryHandle handle, long count) where T : unmanaged + { + if(count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + ResizeIfSmaller(handle, (ulong)count); + } + + /// + /// Resizes the target handle only if the handle is smaller than the requested element count + /// + /// + /// + /// The number of elements to resize to + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ResizeIfSmaller(this MemoryHandle handle, ulong count) where T : unmanaged + { + //Check handle size + if(handle.Length < count) + { + //handle too small, resize + handle.Resize(count); + } + } + +#if TARGET_64_BIT + /// + /// Gets a 64bit friendly span offset for the current + /// + /// + /// + /// The offset (in elements) from the begining of the block + /// The size of the block (in elements) + /// The offset span + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span GetOffsetSpan(this MemoryHandle 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.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(ofPtr, size); + } + throw new ArgumentOutOfRangeException(nameof(size)); + } + /// + /// Gets a 64bit friendly span offset for the current + /// + /// + /// + /// The offset (in elements) from the begining of the block + /// The size of the block (in elements) + /// The offset span + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span GetOffsetSpan(this MemoryHandle block, long offset, int size) where T : unmanaged + { + return offset < 0 ? throw new ArgumentOutOfRangeException(nameof(offset)) : block.GetOffsetSpan((ulong)offset, size); + } + + + /// + /// Gets a window within the current block + /// + /// + /// + /// An offset within the handle + /// The size of the window + /// The new within the block + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SubSequence GetSubSequence(this MemoryHandle block, ulong offset, int size) where T : unmanaged + { + return new SubSequence(block, offset, size); + } +#else + + /// + /// Gets a window within the current block + /// + /// + /// + /// An offset within the handle + /// The size of the window + /// The new within the block + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SubSequence GetSubSequence(this MemoryHandle block, int offset, int size) where T : unmanaged + { + return new SubSequence(block, offset, size); + } + + /// + /// Gets a 64bit friendly span offset for the current + /// + /// + /// + /// The offset (in elements) from the begining of the block + /// The size of the block (in elements) + /// The offset span + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span GetOffsetSpan(this MemoryHandle 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 + + /// + /// Wraps the current instance with a wrapper + /// to allow System.Memory buffer rentals. + /// + /// The unmanged data type to provide allocations from + /// The new heap wrapper. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryPool ToPool(this IUnmangedHeap heap) where T : unmanaged + { + return new PrivateBuffersMemoryPool(heap); + } + + /// + /// Allocates a structure of the specified type on the current unmanged heap and zero's its memory + /// + /// The structure type + /// + /// A pointer to the structure ready for use. + /// Allocations must be freed with + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe T* StructAlloc(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; + } + /// + /// Frees a structure at the specified address from the this heap. + /// This must be the same heap the structure was allocated from + /// + /// The structure type + /// + /// A pointer to the structure + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void StructFree(this IUnmangedHeap heap, T* structPtr) where T : unmanaged + { + IntPtr block = new(structPtr); + //Free block from heap + heap.Free(ref block); + //Clear ref + *structPtr = default; + } + /// + /// Allocates a block of unmanaged memory of the number of elements to store of an unmanged type + /// + /// Unmanaged data type to create a block of + /// + /// The size of the block (number of elements) + /// A flag that zeros the allocated block before returned + /// The unmanaged + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe MemoryHandle Alloc(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(heap, block, elements, zero); + } + /// + /// Allocates a block of unmanaged memory of the number of elements to store of an unmanged type + /// + /// Unmanaged data type to create a block of + /// + /// The size of the block (number of elements) + /// A flag that zeros the allocated block before returned + /// The unmanaged + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryHandle Alloc(this IUnmangedHeap heap, long elements, bool zero = false) where T : unmanaged + { + return elements < 0 ? throw new ArgumentOutOfRangeException(nameof(elements)) : Alloc(heap, (ulong)elements, zero); + } + /// + /// Allocates a buffer from the current heap and initialzies it by copying the initial data buffer + /// + /// + /// + /// The initial data to set the buffer to + /// The initalized block + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryHandle AllocAndCopy(this IUnmangedHeap heap, ReadOnlySpan initialData) where T:unmanaged + { + MemoryHandle handle = heap.Alloc(initialData.Length); + Memory.Copy(initialData, handle, 0); + return handle; + } + + /// + /// Copies data from the input buffer to the current handle and resizes the handle to the + /// size of the buffer + /// + /// The unamanged value type + /// + /// The input buffer to copy data from + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteAndResize(this MemoryHandle handle, ReadOnlySpan input) where T: unmanaged + { + handle.Resize(input.Length); + Memory.Copy(input, handle, 0); + } + + /// + /// Allocates a block of unamanged memory of the number of elements of an unmanaged type, and + /// returns the that must be used cautiously + /// + /// The unamanged value type + /// The heap to allocate block from + /// The number of elements to allocate + /// A flag to zero the initial contents of the buffer + /// The allocated handle of the specified number of elements + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe UnsafeMemoryHandle UnsafeAlloc(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 + + /// + /// Formats and appends a value type to the writer with proper endianess + /// + /// + /// The value to format and append to the buffer + /// + public static void Append(this ref ForwardOnlyWriter buffer, T value) where T: unmanaged + { + //Calc size of structure and fix te size of the buffer + int size = Unsafe.SizeOf(); + Span 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); + } + + /// + /// Formats and appends a value type to the writer with proper endianess + /// + /// + /// The value to format and append to the buffer + /// + public static void Append(this ref ForwardOnlyMemoryWriter buffer, T value) where T : struct + { + //Format value and write to buffer + int size = Unsafe.SizeOf(); + Span 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); + } + + /// + /// Formats and appends the value to end of the buffer + /// + /// + /// The value to format and append to the buffer + /// An optional format argument + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append(this ref ForwardOnlyWriter buffer, T value, ReadOnlySpan 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); + } + + /// + /// Formats and appends the value to end of the buffer + /// + /// + /// The value to format and append to the buffer + /// An optional format argument + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append(this ref ForwardOnlyMemoryWriter buffer, T value, ReadOnlySpan 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); + } + + + + /// + /// 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. + /// + /// + /// Character buffer to encode + /// The offset in the char buffer to begin encoding chars from + /// The number of characers to encode + /// The buffer writer to use + /// true to clear the internal state of the encoder after the conversion; otherwise, false. + /// The actual number of bytes written at the location indicated by the bytes parameter. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBytes(this Encoder enc, char[] chars, int offset, int charCount, ref ForwardOnlyWriter writer, bool flush) + { + return GetBytes(enc, chars.AsSpan(offset, charCount), ref writer, flush); + } + /// + /// 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. + /// + /// + /// The character buffer to encode + /// The buffer writer to use + /// true to clear the internal state of the encoder after the conversion; otherwise, false. + /// The actual number of bytes written at the location indicated by the bytes parameter. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBytes(this Encoder enc, ReadOnlySpan chars, ref ForwardOnlyWriter writer, bool flush) + { + //Encode the characters + int written = enc.GetBytes(chars, writer.Remaining, flush); + //Update the writer position + writer.Advance(written); + return written; + } + /// + /// 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. + /// + /// + /// The character buffer to encode + /// The buffer writer to use + /// The actual number of bytes written at the location indicated by the bytes parameter. + /// + public static int GetBytes(this Encoding encoding, ReadOnlySpan chars, ref ForwardOnlyWriter writer) + { + //Encode the characters + int written = encoding.GetBytes(chars, writer.Remaining); + //Update the writer position + writer.Advance(written); + return written; + } + /// + /// 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. + /// + /// + /// The binary buffer to decode + /// The buffer writer to use + /// The actual number of *characters* written at the location indicated by the chars parameter. + /// + public static int GetChars(this Encoding encoding, ReadOnlySpan bytes, ref ForwardOnlyWriter writer) + { + int charCount = encoding.GetCharCount(bytes); + //Encode the characters + _ = encoding.GetChars(bytes, writer.Remaining); + //Update the writer position + writer.Advance(charCount); + return charCount; + } + + /// + /// Converts the buffer data to a + /// + /// A instance that owns the underlying string memory + public static PrivateString ToPrivate(this ref ForwardOnlyWriter buffer) => new(buffer.ToString(), true); + /// + /// Gets a over the modified section of the internal buffer + /// + /// A over the modified data + public static Span AsSpan(this ref ForwardOnlyWriter buffer) => buffer.Buffer[..buffer.Written]; + + + #endregion + + /// + /// Slices the current array by the specified starting offset to the end + /// of the array + /// + /// The array type + /// + /// The start offset of the new array slice + /// The sliced array + /// + public static T[] Slice(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); + } + /// + /// Slices the current array by the specified starting offset to including the + /// speciifed number of items + /// + /// The array type + /// + /// The start offset of the new array slice + /// The size of the new array + /// The sliced array + /// + public static T[] Slice(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(); + } + //Calc the slice range + Range sliceRange = new(start, start + count); + return RuntimeHelpers.GetSubArray(arr, sliceRange); + } + + /// + /// Creates a new sub-sequence over the target handle. (allows for convient sub span) + /// + /// + /// + /// Intial offset into the handle + /// The sub-sequence of the current handle + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpan(this IMemoryHandle handle, int start) => handle.Span[start..]; + + /// + /// Creates a new sub-sequence over the target handle. (allows for convient sub span) + /// + /// + /// + /// Intial offset into the handle + /// The number of elements within the new sequence + /// The sub-sequence of the current handle + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpan(this IMemoryHandle handle, int start, int count) => handle.Span.Slice(start, count); + + /// + /// Creates a new sub-sequence over the target handle. (allows for convient sub span) + /// + /// + /// + /// Intial offset into the handle + /// The sub-sequence of the current handle + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpan(this in UnsafeMemoryHandle handle, int start) where T: unmanaged => handle.Span[start..]; + + /// + /// Creates a new sub-sequence over the target handle. (allows for convient sub span) + /// + /// + /// + /// Intial offset into the handle + /// The number of elements within the new sequence + /// The sub-sequence of the current handle + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpan(this in UnsafeMemoryHandle handle, int start, int count) where T : unmanaged => handle.Span.Slice(start, count); + + /// + /// Raises an if the current handle + /// has been disposed or set as invalid + /// + /// + /// + [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 +{ + /// + /// Represents a releaser handle for a + /// that has been entered and will be released. Best if used + /// within a using() statment + /// + public readonly struct MutexReleaser : IDisposable, IEquatable + { + private readonly Mutex _mutext; + internal MutexReleaser(Mutex mutex) => _mutext = mutex; + /// + /// Releases the held System.Threading.Mutex once. + /// + public readonly void Dispose() => _mutext.ReleaseMutex(); + /// + /// Releases the held System.Threading.Mutex once. + /// + public readonly void ReleaseMutext() => _mutext.ReleaseMutex(); + + /// + public bool Equals(MutexReleaser other) => _mutext.Equals(other._mutext); + + /// + public override bool Equals(object? obj) => obj is MutexReleaser releaser && Equals(releaser); + + /// + public override int GetHashCode() => _mutext.GetHashCode(); + + /// + public static bool operator ==(MutexReleaser left, MutexReleaser right) => left.Equals(right); + /// + 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 +{ + /// + /// When applied to a delegate, specifies the name of the native method to load + /// + [AttributeUsage(AttributeTargets.Delegate)] + public sealed class SafeMethodNameAttribute : Attribute + { + /// + /// Creates a new + /// + /// The name of the native method + public SafeMethodNameAttribute(string MethodName) => this.MethodName = MethodName; + /// + /// Creates a new , that uses the + /// delegate name as the native method name + /// + public SafeMethodNameAttribute() => MethodName = null; + /// + /// The name of the native method + /// + public string? MethodName { get; } + } + + + /// + /// Contains native library extension methods + /// + public static class SafeLibraryExtensions + { + const string _missMemberExceptionMessage = $"The delegate type is missing the required {nameof(SafeMethodNameAttribute)} to designate the native method to load"; + + /// + /// Loads a native method from the current + /// that has a + /// + /// + /// + /// + /// + /// + /// + public static SafeMethodHandle GetMethod(this SafeLibraryHandle library) where T : Delegate + { + Type t = typeof(T); + //Get the method name attribute + SafeMethodNameAttribute? attr = t.GetCustomAttribute(); + _ = attr ?? throw new MissingMemberException(_missMemberExceptionMessage); + return library.GetMethod(attr.MethodName ?? t.Name); + } + /// + /// Loads a native method from the current + /// that has a + /// + /// + /// + /// + /// + /// + /// + /// + /// The libraries handle count is left unmodified + /// + public static T DangerousGetMethod(this SafeLibraryHandle library) where T: Delegate + { + Type t = typeof(T); + //Get the method name attribute + SafeMethodNameAttribute? attr = t.GetCustomAttribute(); + return string.IsNullOrWhiteSpace(attr?.MethodName) + ? throw new MissingMemberException(_missMemberExceptionMessage) + : library.DangerousGetMethod(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 +{ + /// + /// Represents a releaser handle for a + /// that has been entered and will be released. Best if used + /// within a using() statment + /// + public readonly struct SemSlimReleaser : IDisposable + { + private readonly SemaphoreSlim _semaphore; + internal SemSlimReleaser(SemaphoreSlim semaphore) => _semaphore = semaphore; + /// + /// Releases the System.Threading.SemaphoreSlim object once. + /// + public readonly void Dispose() => _semaphore.Release(); + /// + /// Releases the System.Threading.SemaphoreSlim object once. + /// + /// The previous count of the + /// + /// + 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 line); + + /// + /// Extention methods for string (character buffer) + /// + public static class StringExtensions + { + /// + /// Split a string based on split value and insert into the specified list + /// + /// + /// The value to split the string on + /// The list to output data to + /// String split options + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split(this string value, string splitter, T output, StringSplitOptions options) where T : ICollection + { + Split(value, splitter.AsSpan(), output, options); + } + /// + /// Split a string based on split value and insert into the specified list + /// + /// + /// The value to split the string on + /// The list to output data to + /// String split options + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split(this string value, char splitter, T output, StringSplitOptions options) where T: ICollection + { + //Create span from char pointer + ReadOnlySpan cs = MemoryMarshal.CreateReadOnlySpan(ref splitter, 1); + //Call the split function on the span + Split(value, cs, output, options); + } + /// + /// Split a string based on split value and insert into the specified list + /// + /// + /// The value to split the string on + /// The list to output data to + /// String split options + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split(this string value, ReadOnlySpan splitter, T output, StringSplitOptions options) where T : ICollection + { + Split(value.AsSpan(), splitter, output, options); + } + /// + /// Split a string based on split value and insert into the specified list + /// + /// + /// The value to split the string on + /// The list to output data to + /// String split options + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split(this in ReadOnlySpan value, char splitter, T output, StringSplitOptions options) where T : ICollection + { + //Create span from char pointer + ReadOnlySpan cs = MemoryMarshal.CreateReadOnlySpan(ref splitter, 1); + //Call the split function on the span + Split(in value, cs, output, options); + } + /// + /// Split a based on split value and insert into the specified list + /// + /// + /// The value to split the string on + /// The list to output data to + /// String split options + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split(this in ReadOnlySpan value, ReadOnlySpan splitter, T output, StringSplitOptions options) where T : ICollection + { + //Create a local function that adds the split strings to the list + static void SplitFound(ReadOnlySpan split, T output) => output.Add(split.ToString()); + //Invoke the split function with the local callback method + Split(in value, splitter, options, SplitFound, output); + } + /// + /// Split a based on split value and pass it to the split delegate handler + /// + /// + /// The sequence to split the string on + /// String split options + /// The action to invoke when a split segment has been found + /// The state to pass to the callback handler + /// + public static void Split(this in ReadOnlySpan value, ReadOnlySpan splitter, StringSplitOptions options, ReadOnlySpanAction splitCb, T state) + { + _ = splitCb ?? throw new ArgumentNullException(nameof(splitCb)); + //Get span over string + ForwardOnlyReader 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 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 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); + } + } + } + + /// + /// Split a based on split value and pass it to the split delegate handler + /// + /// + /// The character to split the string on + /// String split options + /// The action to invoke when a split segment has been found + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split(this in ReadOnlySpan value, char splitter, StringSplitOptions options, ReadOnlySpanAction splitCb, T state) + { + //Alloc a span for char + ReadOnlySpan cs = MemoryMarshal.CreateReadOnlySpan(ref splitter, 1); + //Call the split function on the span + Split(in value, cs, options, splitCb, state); + } + /// + /// Split a based on split value and pass it to the split delegate handler + /// + /// + /// The sequence to split the string on + /// String split options + /// The action to invoke when a split segment has been found + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split(this in ReadOnlySpan value, ReadOnlySpan splitter, StringSplitOptions options, StatelessSpanAction splitCb) + { + //Create a SpanSplitDelegate with the non-typed delegate as the state argument + static void ssplitcb(ReadOnlySpan param, StatelessSpanAction callback) => callback(param); + //Call split with the new callback delegate + Split(in value, splitter, options, ssplitcb, splitCb); + } + /// + /// Split a based on split value and pass it to the split delegate handler + /// + /// + /// The character to split the string on + /// String split options + /// The action to invoke when a split segment has been found + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Split(this in ReadOnlySpan value, char splitter, StringSplitOptions options, StatelessSpanAction splitCb) + { + //Create a SpanSplitDelegate with the non-typed delegate as the state argument + static void ssplitcb(ReadOnlySpan param, StatelessSpanAction callback) => callback(param); + //Call split with the new callback delegate + Split(in value, splitter, options, ssplitcb, splitCb); + } + + /// + /// Gets the index of the end of the found sequence + /// + /// + /// Sequence to search for within the current sequence + /// the index of the end of the sequenc + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int EndOf(this in ReadOnlySpan data, ReadOnlySpan search) + { + int index = data.IndexOf(search); + return index > -1 ? index + search.Length : -1; + } + /// + /// Gets the index of the end of the found character + /// + /// + /// Character to search for within the current sequence + /// the index of the end of the sequence + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int EndOf(this in ReadOnlySpan data, char search) + { + int index = data.IndexOf(search); + return index > -1 ? index + 1 : -1; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOf(this in Memory data, byte search) => data.Span.IndexOf(search); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOf(this in Memory data, ReadOnlySpan search) => data.Span.IndexOf(search); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOf(this in Memory data, ReadOnlyMemory search) => IndexOf(data, search.Span); + + /// + /// 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 + /// + /// + /// The delimiting character + /// The segment of data before the search character, or the entire segment if not found + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan SliceBeforeParam(this in ReadOnlySpan 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; + } + /// + /// 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 + /// + /// + /// The delimiting character sequence + /// The segment of data before the search character, or the entire if the seach sequence is not found + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan SliceBeforeParam(this in ReadOnlySpan data, ReadOnlySpan 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; + } + /// + /// Gets the remaining segment of data after the specified search character or + /// if the search character is not found within the current segment + /// + /// + /// The character to search for within the segment + /// The segment of data after the search character or if not found + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan SliceAfterParam(this in ReadOnlySpan 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.Empty; + } + /// + /// Gets the remaining segment of data after the specified search sequence or + /// if the search sequence is not found within the current segment + /// + /// + /// The sequence to search for within the segment + /// The segment of data after the search sequence or if not found + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan SliceAfterParam(this in ReadOnlySpan data, ReadOnlySpan 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.Empty; + } + /// + /// Trims any leading or trailing '\r'|'\n'|' '(whitespace) characters from the segment + /// + /// The trimmed segment + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan TrimCRLF(this in ReadOnlySpan 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]; + } + + /// + /// Replaces a character sequence within the buffer + /// + /// The character buffer to process + /// The sequence to search for + /// The sequence to write in the place of the search parameter + /// + public static int Replace(this ref Span buffer, ReadOnlySpan search, ReadOnlySpan replace) + { + ForwardOnlyWriter writer = new (buffer); + writer.Replace(search, replace); + return writer.Written; + } + + /// + /// Replaces a character sequence within the writer + /// + /// + /// The sequence to search for + /// The sequence to write in the place of the search parameter + /// + public static void Replace(this ref ForwardOnlyWriter writer, ReadOnlySpan search, ReadOnlySpan replace) + { + Span 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); + } + /// + /// Replaces very ocurrance of character sequence within a buffer with another sequence of the same length + /// + /// + /// The sequence to search for + /// The sequence to replace the found sequence with + /// + public static void ReplaceInPlace(this Span buffer, ReadOnlySpan search, ReadOnlySpan 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 +{ + + /// + /// Provides extension methods to common threading and TPL library operations + /// + public static class ThreadingExtensions + { + /// + /// Allows an to execute within a scope limited context + /// + /// The resource type + /// + /// The function body that will execute with controlled access to the resource + public static void EnterSafeContext(this OpenResourceHandle rh, Action safeCallback) + { + using (rh) + { + safeCallback(rh.Resource); + } + } + + /// + /// Asynchronously waits to enter the while observing a + /// and getting a releaser handle + /// + /// + /// A token to cancel the operation + /// A releaser handle that may be disposed to release the semaphore + /// + /// + public static async Task GetReleaserAsync(this SemaphoreSlim semaphore, CancellationToken cancellationToken = default) + { + await semaphore.WaitAsync(cancellationToken); + return new SemSlimReleaser(semaphore); + } + /// + /// Asynchronously waits to enter the using a 32-bit signed integer to measure the time intervale + /// and getting a releaser handle + /// + /// + /// A the maximum amount of time in milliseconds to wait to enter the semaphore + /// A releaser handle that may be disposed to release the semaphore + /// + /// + public static async Task 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"); + } + + /// + /// Blocks the current thread until it can enter the + /// + /// + /// A releaser handler that releases the semaphore when disposed + /// + public static SemSlimReleaser GetReleaser(this SemaphoreSlim semaphore) + { + semaphore.Wait(); + return new SemSlimReleaser(semaphore); + } + /// + /// Blocks the current thread until it can enter the + /// + /// + /// A the maximum amount of time in milliseconds to wait to enter the semaphore + /// A releaser handler that releases the semaphore when disposed + /// + /// + 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"); + } + + /// + /// Blocks the current thread until it can enter the + /// + /// + /// A releaser handler that releases the semaphore when disposed + /// + /// + public static MutexReleaser Enter(this Mutex mutex) + { + mutex.WaitOne(); + return new MutexReleaser(mutex); + } + /// + /// Blocks the current thread until it can enter the + /// + /// + /// A the maximum amount of time in milliseconds to wait to enter the semaphore + /// A releaser handler that releases the semaphore when disposed + /// + /// + 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 TrueCompleted = Task.FromResult(true); + private static readonly Task FalseCompleted = Task.FromResult(false); + + /// + /// Asynchronously waits for a the to receive a signal. This method spins until + /// a thread yield will occur, then asynchronously yields. + /// + /// + /// The timeout interval in milliseconds + /// + /// A task that compeletes when the wait handle receives a signal or times-out, + /// the result of the awaited task will be true if the signal is received, or + /// false if the timeout interval expires + /// + /// + /// + /// + public static Task 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 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 task, object? taskCompletion) + { + RegisteredWaitHandle registration = (taskCompletion as RegisteredWaitHandle)!; + registration.Unregister(null); + task.Dispose(); + } + private static void TaskCompletionCallback(object? tcsState, bool timedOut) + { + TaskCompletionSource completion = (tcsState as TaskCompletionSource)!; + //Set the result of the wait handle timeout + _ = completion.TrySetResult(!timedOut); + } + + + /// + /// Registers a callback method that will be called when the token has been cancelled. + /// This method waits indefinitely for the token to be cancelled. + /// + /// + /// The callback method to invoke when the token has been cancelled + /// A task that may be unobserved, that completes when the token has been cancelled + 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 + { + /// + /// Attempts to stop the timer + /// + /// True if the timer was successfully modified, false otherwise + public static bool Stop(this Timer timer) => timer.Change(Timeout.Infinite, Timeout.Infinite); + + /// + /// Attempts to stop an active timer and prepare a configured to restore the state of the timer to the specified timespan + /// + /// + /// representing the amount of time the timer should wait before invoking the callback function + /// A new if the timer was stopped successfully that will resume the timer when closed, null otherwise + public static OpenHandle Stop(this Timer timer, TimeSpan resumeTime) + { + return timer.Change(Timeout.Infinite, Timeout.Infinite) ? new CallbackOpenHandle(() => timer.Change(resumeTime, Timeout.InfiniteTimeSpan)) : null; + } + + /// + /// Attempts to reset and start a timer + /// + /// + /// to wait before the timer event is fired + /// True if the timer was successfully modified + public static bool Restart(this Timer timer, TimeSpan wait) => timer.Change(wait, Timeout.InfiniteTimeSpan); + + /// + /// Attempts to reset and start a timer + /// + /// + /// Time in milliseconds to wait before the timer event is fired + /// True if the timer was successfully modified + 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 = "")] + public static class VnStringExtensions + { + /// + /// Derermines if the character exists within the instance + /// + /// + /// The value to find + /// True if the character exists within the instance + /// + public static bool Contains(this VnString str, char value) => str.AsSpan().Contains(value); + /// + /// Derermines if the sequence exists within the instance + /// + /// + /// The sequence to find + /// + /// True if the character exists within the instance + /// + + public static bool Contains(this VnString str, ReadOnlySpan value, StringComparison stringComparison) => str.AsSpan().Contains(value, stringComparison); + + /// + /// Searches for the first occurrance of the specified character within the current instance + /// + /// + /// The character to search for within the instance + /// The 0 based index of the occurance, -1 if the character was not found + /// + public static int IndexOf(this VnString str, char value) => str.IsEmpty ? -1 : str.AsSpan().IndexOf(value); + /// + /// Searches for the first occurrance of the specified sequence within the current instance + /// + /// + /// The sequence to search for + /// The 0 based index of the occurance, -1 if the sequence was not found + /// + public static int IndexOf(this VnString str, ReadOnlySpan search) + { + //Using spans to avoid memory leaks... + ReadOnlySpan self = str.AsSpan(); + return self.IndexOf(search); + } + /// + /// Searches for the first occurrance of the specified sequence within the current instance + /// + /// + /// The sequence to search for + /// The type to use in searchr + /// The 0 based index of the occurance, -1 if the sequence was not found + /// + public static int IndexOf(this VnString str, ReadOnlySpan search, StringComparison comparison) + { + //Using spans to avoid memory leaks... + ReadOnlySpan self = str.AsSpan(); + return self.IndexOf(search, comparison); + } + /// + /// Searches for the 0 based index of the first occurance of the search parameter after the start index. + /// + /// + /// The sequence of data to search for + /// The lower boundry of the search area + /// The absolute index of the first occurrance within the instance, -1 if the sequency was not found in the windowed segment + /// + /// + public static int IndexOf(this VnString str, ReadOnlySpan search, int start) + { + if (start < 0) + { + throw new ArgumentOutOfRangeException(nameof(start), "Start cannot be less than 0"); + } + //Get shifted window + ReadOnlySpan self = str.AsSpan()[start..]; + //Check indexof + int index = self.IndexOf(search); + return index > -1 ? index + start : -1; + } + + /// + /// Returns the realtive index after the specified sequence within the instance + /// + /// + /// The sequence to search for + /// The index after the found sequence within the string, -1 if the sequence was not found within the instance + /// + public static int EndOf(this VnString str, ReadOnlySpan 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; + } + + /// + /// Allows for trimming whitespace characters in a realtive sequence from + /// within a buffer defined by the start and end parameters + /// and returning the trimmed entry. + /// + /// + /// The starting position within the sequence to trim + /// The end of the sequence to trim + /// The trimmed instance as a child of the original entry + /// + /// + public static VnString AbsoluteTrim(this VnString data, int start, int end) + { + AbsoluteTrim(data, ref start, ref end); + return data[start..end]; + } + /// + /// Finds whitespace characters within the sequence defined between start and end parameters + /// and adjusts the specified window to "trim" whitespace + /// + /// + /// The starting position within the sequence to trim + /// The end of the sequence to trim + /// + /// + public static void AbsoluteTrim(this VnString data, ref int start, ref int end) + { + ReadOnlySpan 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--; + } + } + /// + /// Allows for trimming whitespace characters in a realtive sequence from + /// within a buffer and returning the trimmed entry. + /// + /// + /// The starting position within the sequence to trim + /// The trimmed instance as a child of the original entry + /// + /// + public static VnString AbsoluteTrim(this VnString data, int start) => AbsoluteTrim(data, start, data.Length); + /// + /// Trims leading or trailing whitespace characters and returns a new child instance + /// without leading or trailing whitespace + /// + /// A child of the current instance without leading or trailing whitespaced + /// + public static VnString RelativeTirm(this VnString data) => AbsoluteTrim(data, 0); + + /// + /// Allows for enumeration of segments of data within the specified instance that are + /// split by the search parameter + /// + /// + /// The sequence of data to delimit segments + /// The options used to split the string instances + /// An iterator to enumerate the split segments + /// + public static IEnumerable Split(this VnString data, ReadOnlyMemory 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]; + } + } + } + + /// + /// Trims any leading or trailing '\r'|'\n'|' '(whitespace) characters from the segment + /// + /// The trimmed segment + /// + public static VnString TrimCRLF(this VnString data) + { + ReadOnlySpan 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]; + } + + /// + /// Unoptimized character enumerator. You should use to enumerate the unerlying data. + /// + /// The next character in the sequence + /// + public static IEnumerator GetEnumerator(this VnString data) + { + int index = 0; + while (index < data.Length) + { + yield return data[index++]; + } + } + /// + /// Converts the current handle to a , a zero-alloc immutable wrapper + /// for a memory handle + /// + /// + /// The number of characters from the handle to reference (length of the string) + /// The new wrapper + /// + /// + public static VnString ToVnString(this MemoryHandle 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); + } + /// + /// Converts the current handle to a , a zero-alloc immutable wrapper + /// for a memory handle + /// + /// + /// The new wrapper + /// + /// + public static VnString ToVnString(this MemoryHandle handle) + { + return VnString.ConsumeHandle(handle, 0, handle.IntLength); + } + /// + /// Converts the current handle to a , a zero-alloc immutable wrapper + /// for a memory handle + /// + /// + /// The offset in characters that represents the begining of the string + /// The number of characters from the handle to reference (length of the string) + /// The new wrapper + /// + /// + public static VnString ToVnString(this MemoryHandle 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 -- cgit