diff options
author | vnugent <public@vaughnnugent.com> | 2024-07-28 19:15:04 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-07-28 19:15:04 -0400 |
commit | 7be5d6648e633ba46a270ca5784de6f4a5a4e0a9 (patch) | |
tree | a6b53de82f12e6778ede2b3974073a6c3b51aace /lib/Utils | |
parent | 1b590c2517fef110564943ed8a10edd11fa758b0 (diff) |
Squashed commit of the following:
commit a4dacd2909426bf628c1eee1253cc5c8a01e2691
Author: vnugent <public@vaughnnugent.com>
Date: Sat Jul 27 22:41:04 2024 -0400
package updates
commit f836e09981866f5c9f2ae46990d11b186a7d12bb
Author: vnugent <public@vaughnnugent.com>
Date: Wed Jul 24 19:15:54 2024 -0400
chore: Remove argon2 docs & optional tcp resuse
commit b9b892ab2143b0ab92e4dcf0a8b043c5c6c17271
Author: vnugent <public@vaughnnugent.com>
Date: Sun Jul 21 20:57:01 2024 -0400
fix spelling Enqueue and deprecate mispelled version
commit 21ffa816f18be4b765ad740ed5d93346ec3b1fda
Author: vnugent <public@vaughnnugent.com>
Date: Sat Jul 20 19:44:31 2024 -0400
static arugment list parsing functions
commit 85cd6793818a3edd0a963bb4829a960ee6b0e022
Author: vnugent <public@vaughnnugent.com>
Date: Mon Jul 15 18:58:06 2024 -0400
chore: Just some minor checks and adjustments
commit abfb5761ee381b7e1e5342a5525ceca8c8fd81dd
Author: vnugent <public@vaughnnugent.com>
Date: Thu Jul 4 23:57:37 2024 -0400
analyzer pass
commit 4a96dbb924f2b5bf80293e4054f221efe67151dd
Author: vnugent <public@vaughnnugent.com>
Date: Thu Jul 4 22:45:28 2024 -0400
package updates
commit 38ad7d923fa8d9e463d4aaa8e35f021086a03f2d
Author: vnugent <public@vaughnnugent.com>
Date: Thu Jul 4 16:20:48 2024 -0400
mimalloc merge upstream upgrades
commit 981ba286e4793de95bf65e6588313411344c4d53
Author: vnugent <public@vaughnnugent.com>
Date: Thu Jul 4 16:04:03 2024 -0400
refactor: Refactor extensions with perf updates
commit 6b8c67888731f7dd210acdb2b1160cdbdbe30d47
Author: vnugent <public@vaughnnugent.com>
Date: Fri Jun 28 15:48:22 2024 -0400
refactor: Update service stack to reflect new loading patterns
commit 12391e9a207b60b41a074600fc2373ad3eb1c3ab
Author: vnugent <public@vaughnnugent.com>
Date: Wed Jun 26 21:01:15 2024 -0400
feat(server): Server arch update, Memory struct access
commit 92e182ceaf843f8d859d38faa8b2c0ff53207ff6
Author: vnugent <public@vaughnnugent.com>
Date: Fri Jun 21 16:02:34 2024 -0400
feat: Multi transport listeners
commit ee3620b8168a42c8e571e853c751ad5999a9b907
Author: vnugent <public@vaughnnugent.com>
Date: Tue Jun 18 21:17:28 2024 -0400
feat: Add file path caching support
commit ff0926be56fc6eafdce36411847d73bf4ce9f183
Author: vnugent <public@vaughnnugent.com>
Date: Sun Jun 16 13:08:31 2024 -0400
feat: Allow multiple plugin loading directories
commit 07ddf6738d32127926d07b1366e56d2a2308b53b
Author: vnugent <public@vaughnnugent.com>
Date: Sun Jun 16 01:12:07 2024 -0400
perf: Absolutely yuge perf boosts
commit ff15c05a9c3e632c39f3889820fb7d889342b452
Author: vnugent <public@vaughnnugent.com>
Date: Fri Jun 14 14:16:24 2024 -0400
fix: Improper request buffer property assignment
commit 7d2987f1d4048c30808a85798e32c99747f6cfe3
Author: vnugent <public@vaughnnugent.com>
Date: Thu Jun 13 21:57:34 2024 -0400
perf: Async pre-buffer to avoid sync buffer
commit 75c1d0cbf9a5a7856c544671a45f1b4312ffe7ce
Author: vnugent <public@vaughnnugent.com>
Date: Tue Jun 11 22:11:45 2024 -0400
feat: Add a default site adapater and interceptors
commit a7c739b7db9a17622cee751fe0e8a10e4b84b48b
Author: vnugent <public@vaughnnugent.com>
Date: Sun Jun 9 13:05:12 2024 -0400
chore: Package updated
commit b4b506a4b6c7c1e90b5b0980e4cfe0460e7546a2
Author: vnugent <public@vaughnnugent.com>
Date: Sat Jun 8 21:54:52 2024 -0400
some minor touchups
commit 2160510fcc22a8574b0090fd91ca29072f45ab59
Author: vnugent <public@vaughnnugent.com>
Date: Fri May 31 15:12:20 2024 -0400
refactor: Immutable tcp listeners
commit 51cb4eb93e4f1b4c47d35b105e72af1fe771abcc
Author: vnugent <public@vaughnnugent.com>
Date: Thu May 30 17:31:16 2024 -0400
refactor: minor non-breaking changes to VNEncoding
commit 768ddc1eb949266d693f96c67d734e881bd59374
Merge: 9a835fe 1b590c2
Author: vnugent <public@vaughnnugent.com>
Date: Wed May 22 17:50:57 2024 -0400
Merge branch 'main' into develop
commit 9a835fe12c9586ab8dd44d7c96fef4a2d6017e4b
Author: vnugent <public@vaughnnugent.com>
Date: Fri May 17 18:27:03 2024 -0400
chore: Update mimmaloc v2.1.6, update fPIC & cleanup
commit 3b7004b88acfc7f7baa3a8857a5a2f7cf3dd560e
Author: vnugent <public@vaughnnugent.com>
Date: Fri May 17 16:03:28 2024 -0400
feat: Added ReadFileDataAsync function
commit 9a964795757bf0da4dd7fcab15ad304f4ea3fdf1
Author: vnugent <public@vaughnnugent.com>
Date: Wed May 15 21:57:39 2024 -0400
refactor: Harden some argon2 password hashing
commit 4035c838c1508af0aa7e767a97431a692958ce1c
Author: vnugent <public@vaughnnugent.com>
Date: Sun May 12 16:55:32 2024 -0400
perf: Utils + http perf mods
commit f4f0d4f74250257991c57bfae74c4852c7e1ae46
Author: vnugent <public@vaughnnugent.com>
Date: Thu May 2 15:22:53 2024 -0400
feat: Buff middleware handlers
|
| Added implicit support for middleware post processing of files before the filehandler closes the connection. Also cleaned up some project file stuff
commit f0b7dca107659df1d7d4631fdbd2aae9d716d053
Merge: 8c4a45e 107b058
Author: vnugent <public@vaughnnugent.com>
Date: Sat Apr 20 12:24:05 2024 -0400
Merge branch 'main' into develop
commit 8c4a45e384accf92b1b6d748530e8d46f7de40d6
Author: vnugent <public@vaughnnugent.com>
Date: Sat Apr 20 11:10:30 2024 -0400
refactor: Overhaul C libraries and fix builds
commit 42ff77080d10b0fc9fecbbc46141e8e23a1d066a
Author: vnugent <public@vaughnnugent.com>
Date: Sat Apr 20 00:45:57 2024 -0400
fix!: Middlware array, multiple cookie set, and cookie check
commit 97e82b9d66f387f9e6d21d88ddc7a8ab8693149c
Merge: 4ca5791 e07537a
Author: vnugent <public@vaughnnugent.com>
Date: Tue Apr 2 13:34:22 2024 -0400
Merge branch 'main' into develop
commit 4ca5791ed67b9834bdbd010206b30373e4705e9b
Author: vnugent <public@vaughnnugent.com>
Date: Tue Apr 2 13:32:12 2024 -0400
fix: Missed ! on null pointer check
commit 9b4036377c52200c6488c98180d69a0e63321f97
Author: vnugent <public@vaughnnugent.com>
Date: Tue Apr 2 13:22:29 2024 -0400
fix: Fix _In_ macro for compression public api
commit 53a7b4b5c5b67b4a4e06e1d9098cac4bcd6afd7c
Merge: 448a93b 21130c8
Author: vnugent <public@vaughnnugent.com>
Date: Sun Mar 31 17:01:15 2024 -0400
Merge branch 'main' into develop
commit 448a93bb1d18d032087afe2476ffccb98634a54c
Author: vnugent <public@vaughnnugent.com>
Date: Sun Mar 31 16:56:51 2024 -0400
ci: fix third-party dir cleanup
commit 9afed1427472da1ea13079f98dbe27339e55ee7d
Author: vnugent <public@vaughnnugent.com>
Date: Sun Mar 31 16:43:15 2024 -0400
perf: Deprecate unsafememoryhandle span extensions
commit 3ff90da4f02af47ea6d233fdd4445337ebe36452
Author: vnugent <public@vaughnnugent.com>
Date: Sat Mar 30 21:36:18 2024 -0400
refactor: Updates, advanced tracing, http optimizations
commit 8d6b79b5ae309b36f265ba81529bcef8bfcd7414
Merge: 6c1667b 5585915
Author: vnugent <public@vaughnnugent.com>
Date: Sun Mar 24 21:01:31 2024 -0400
Merge branch 'main' into develop
commit 6c1667be23597513537f8190e2f55d65eb9b7c7a
Author: vnugent <public@vaughnnugent.com>
Date: Fri Mar 22 12:01:53 2024 -0400
refactor: Overhauled native library loading and lazy init
commit ebf688f2f974295beabf7b5def7e6f6f150551d0
Author: vnugent <public@vaughnnugent.com>
Date: Wed Mar 20 22:16:17 2024 -0400
refactor: Update compression header files and macros + Ci build
commit 9c7b564911080ccd5cbbb9851a0757b05e1e9047
Author: vnugent <public@vaughnnugent.com>
Date: Tue Mar 19 21:54:49 2024 -0400
refactor: JWK overhaul & add length getter to FileUpload
commit 6d8c3444e09561e5957491b3cc1ae858e0abdd14
Author: vnugent <public@vaughnnugent.com>
Date: Mon Mar 18 16:13:20 2024 -0400
feat: Add FNV1a software checksum and basic correction tests
commit 00d182088cecefc08ca80b1faee9bed3f215f40b
Author: vnugent <public@vaughnnugent.com>
Date: Fri Mar 15 01:05:27 2024 -0400
chore: #6 Use utils filewatcher instead of built-in
commit d513c10d9895c6693519ef1d459c6a5a76929436
Author: vnugent <public@vaughnnugent.com>
Date: Sun Mar 10 21:58:14 2024 -0400
source tree project location updated
Diffstat (limited to 'lib/Utils')
20 files changed, 856 insertions, 564 deletions
diff --git a/lib/Utils/src/ArgumentList.cs b/lib/Utils/src/ArgumentList.cs index c02ebee..0f092c6 100644 --- a/lib/Utils/src/ArgumentList.cs +++ b/lib/Utils/src/ArgumentList.cs @@ -41,18 +41,16 @@ namespace VNLib.Utils /// </summary> /// <param name="args">The array of arguments to clone</param> /// <exception cref="ArgumentNullException"></exception> - public ArgumentList(string[] args) - { - ArgumentNullException.ThrowIfNull(args); - _args = args.ToList(); - } + [Obsolete("Deprecated in preference to ctor(IEnumerable<string>)")] + public ArgumentList(string[] args) : this(args as IEnumerable<string>) + { } /// <summary> /// Initalizes the argument parser by copying the given argument list /// </summary> /// <param name="args">The argument list to clone</param> /// <exception cref="ArgumentNullException"></exception> - public ArgumentList(IReadOnlyList<string> args) + public ArgumentList(IEnumerable<string> args) { ArgumentNullException.ThrowIfNull(args); _args = args.ToList(); @@ -73,9 +71,9 @@ namespace VNLib.Utils /// <summary> /// Determines of the given argument is present in the argument list /// </summary> - /// <param name="arg"></param> + /// <param name="arg">The name of the argument to check existence of</param> /// <returns>A value that indicates if the argument is present in the list</returns> - public bool HasArgument(string arg) => _args.Contains(arg); + public bool HasArgument(string arg) => HasArgument(_args, arg); /// <summary> /// Determines if the argument is present in the argument list and @@ -91,16 +89,91 @@ namespace VNLib.Utils /// </summary> /// <param name="arg">The argument to get following value of</param> /// <returns>The argument value if found</returns> - public string? GetArgument(string arg) - { - int index = _args.IndexOf(arg); - return index == -1 || index + 1 >= _args.Count ? null : this[index + 1]; - } + public string? GetArgument(string arg) => GetArgument(_args, arg); ///<inheritdoc/> public IEnumerator<string> GetEnumerator() => _args.GetEnumerator(); ///<inheritdoc/> IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// <summary> + /// Captures the command line arguments from the currently executing process + /// and returns them as an ArgumentList + /// </summary> + /// <returns>The <see cref="ArgumentList"/> containing the current process's argument list</returns> + public static ArgumentList CaptureCurrentArgs() + { + /* + * Capture the current command line arguments and + * pop the first argument which is always the program + * name + */ + string[] strings = Environment.GetCommandLineArgs(); + return new ArgumentList(strings.Skip(1)); + } + + /// <summary> + /// Determines of the given argument is present in the argument list + /// </summary> + /// <param name="argsList">The collection to search for the arugment within</param> + /// <param name="argName">The name of the argument to check existence of</param> + /// <returns>A value that indicates if the argument is present in the list</returns> + public static bool HasArgument<T>(T argsList, string argName) where T: IEnumerable<string> + => argsList.Contains(argName, StringComparer.OrdinalIgnoreCase); + + /// <summary> + /// Determines if the argument is present in the argument list and + /// has a non-null value following it. + /// </summary> + /// <param name="argsList">The collection to search for the arugment within</param> + /// <param name="argName">The name of the argument to check existence of</param> + /// <returns>A value that indicates if a non-null argument is present in the list</returns> + public static bool HasArgumentValue<T>(T argsList, string argName) where T : IEnumerable<string> + => GetArgument(argsList, argName) != null; + + /// <summary> + /// Gets the value following the specified argument, or + /// null no value follows the specified argument + /// </summary> + /// <param name="argsList">The collection to search for the arugment within</param> + /// <param name="argName">The name of the argument to check existence of</param> + /// <returns>The argument value if found</returns> + public static string? GetArgument<T>(T argsList, string argName) where T : IEnumerable<string> + { + ArgumentNullException.ThrowIfNull(argsList); + + /* + * Try to optimize some fetching for types that have + * better performance for searching/indexing + */ + if (argsList is IList<string> argList) + { + int index = argList.IndexOf(argName); + return index == -1 || index + 1 >= argList.Count + ? null + : argList[index + 1]; + } + else if(argsList is string[] argsArr) + { + return findInArray(argsArr, argName); + } + else + { + //TODO use linq instead of converting to array on every call + return findInArray( + argsList.ToArray(), + argName + ); + } + + static string? findInArray(string[] argsArr, string argName) + { + int index = Array.IndexOf(argsArr, argName); + return index == -1 || index + 1 >= argsArr.Length + ? null + : argsArr[index + 1]; + } + } } }
\ No newline at end of file diff --git a/lib/Utils/src/Async/AsyncQueue.cs b/lib/Utils/src/Async/AsyncQueue.cs index e94d08e..a64733f 100644 --- a/lib/Utils/src/Async/AsyncQueue.cs +++ b/lib/Utils/src/Async/AsyncQueue.cs @@ -60,7 +60,9 @@ namespace VNLib.Utils.Async /// </summary> /// <param name="singleWriter">A value that specifies only a single thread be enqueing items?</param> /// <param name="singleReader">A value that specifies only a single thread will be dequeing</param> - /// <param name="capacity">The maxium number of items to enque without failing</param> + /// <param name="capacity"> + /// The maxium number of items to enque without failing. If set to <see cref="int.MaxValue"/> maximum is disabled + /// </param> public AsyncQueue(bool singleWriter, bool singleReader, int capacity = int.MaxValue) { if(capacity == int.MaxValue) @@ -102,7 +104,7 @@ namespace VNLib.Utils.Async public AsyncQueue(BoundedChannelOptions options) => _channel = Channel.CreateBounded<T>(options); /// <inheritdoc/> - public bool TryEnque(T item) => _channel.Writer.TryWrite(item); + public bool TryEnqueue(T item) => _channel.Writer.TryWrite(item); /// <inheritdoc/> /// <exception cref="ObjectDisposedException"></exception> @@ -119,5 +121,7 @@ namespace VNLib.Utils.Async /// <inheritdoc/> /// <exception cref="ObjectDisposedException"></exception> public bool TryPeek([MaybeNullWhen(false)] out T result) => _channel.Reader.TryPeek(out result); + + bool IAsyncQueue<T>.TryEnque(T item) => TryEnqueue(item); } } diff --git a/lib/Utils/src/Async/IAsyncQueue.cs b/lib/Utils/src/Async/IAsyncQueue.cs index ab786f1..677f34f 100644 --- a/lib/Utils/src/Async/IAsyncQueue.cs +++ b/lib/Utils/src/Async/IAsyncQueue.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Utils @@ -22,6 +22,7 @@ * along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. */ +using System; using System.Threading; using System.Threading.Tasks; using System.Diagnostics.CodeAnalysis; @@ -41,6 +42,14 @@ namespace VNLib.Utils.Async /// </summary> /// <param name="item">The item to eqneue</param> /// <returns>True if the queue can accept another item, false otherwise</returns> + bool TryEnqueue(T item); + + /// <summary> + /// Attemts to enqueue an item if the queue has the capacity + /// </summary> + /// <param name="item">The item to eqneue</param> + /// <returns>True if the queue can accept another item, false otherwise</returns> + [Obsolete("Use TryEnqueue instead")] bool TryEnque(T item); /// <summary> diff --git a/lib/Utils/src/Extensions/MemoryExtensions.cs b/lib/Utils/src/Extensions/MemoryExtensions.cs index c433527..f2399a2 100644 --- a/lib/Utils/src/Extensions/MemoryExtensions.cs +++ b/lib/Utils/src/Extensions/MemoryExtensions.cs @@ -48,19 +48,8 @@ namespace VNLib.Utils.Extensions /// <param name="size">The minimum size array to allocate</param> /// <param name="zero">Should elements from 0 to size be set to default(T)</param> /// <returns>A new <see cref="OpenResourceHandle{T}"/> encapsulating the rented array</returns> - public static UnsafeMemoryHandle<T> UnsafeAlloc<T>(this ArrayPool<T> pool, int size, bool zero = false) where T : unmanaged - { - ArgumentNullException.ThrowIfNull(pool); - - T[] array = pool.Rent(size); - - if (zero) - { - MemoryUtil.InitializeBlock(array, (uint)size); - } - - return new(pool, array, size); - } + public static UnsafeMemoryHandle<T> UnsafeAlloc<T>(this ArrayPool<T> pool, int size, bool zero = false) where T : unmanaged + => MemoryUtil.UnsafeAlloc<T>(pool, size, zero); /// <summary> /// Rents a new array and stores it as a resource within an <see cref="OpenResourceHandle{T}"/> to return the @@ -72,19 +61,7 @@ namespace VNLib.Utils.Extensions /// <param name="zero">Should elements from 0 to size be set to default(T)</param> /// <returns>A new <see cref="OpenResourceHandle{T}"/> encapsulating the rented array</returns> public static IMemoryHandle<T> SafeAlloc<T>(this ArrayPool<T> pool, int size, bool zero = false) where T : struct - { - ArgumentNullException.ThrowIfNull(pool); - - T[] array = pool.Rent(size); - - if (zero) - { - MemoryUtil.InitializeBlock(array, (uint)size); - } - - //Use the array pool buffer wrapper to return the array to the pool when the handle is disposed - return new ArrayPoolBuffer<T>(pool, array, size); - } + => MemoryUtil.SafeAlloc<T>(pool, size, zero); /// <summary> /// Retreives a buffer that is at least the reqested length, and clears the array from 0-size. @@ -111,13 +88,6 @@ namespace VNLib.Utils.Extensions } /// <summary> - /// Copies the characters within the memory handle to a <see cref="string"/> - /// </summary> - /// <returns>The string representation of the buffer</returns> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string ToString<T>(this T charBuffer) where T : IMemoryHandle<char> => charBuffer.Span.ToString(); - - /// <summary> /// Wraps the <see cref="IMemoryHandle{T}"/> instance in System.Buffers.MemoryManager /// wrapper to provide <see cref="Memory{T}"/> buffers from umanaged handles. /// </summary> @@ -131,7 +101,8 @@ namespace VNLib.Utils.Extensions /// <remarks>NOTE: This wrapper now manages the lifetime of the current handle</remarks> /// <exception cref="ArgumentNullException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MemoryManager<T> ToMemoryManager<T>(this IMemoryHandle<T> handle, bool ownsHandle) => new SysBufferMemoryManager<T>(handle, ownsHandle); + public static MemoryManager<T> ToMemoryManager<T>(this IMemoryHandle<T> handle, bool ownsHandle) + => new SysBufferMemoryManager<T>(handle, ownsHandle); /// <summary> /// Allows direct allocation of a fixed size <see cref="MemoryManager{T}"/> from a <see cref="IUnmangedHeap"/> instance @@ -168,7 +139,8 @@ namespace VNLib.Utils.Extensions /// </returns> /// <exception cref="OverflowException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetIntLength<T>(this IMemoryHandle<T> handle) => Convert.ToInt32(handle.Length); + public static int GetIntLength<T>(this IMemoryHandle<T> handle) + => Convert.ToInt32(handle.Length); /// <summary> /// Gets the integer length (number of elements) of the <see cref="UnsafeMemoryHandle{T}"/> @@ -181,7 +153,8 @@ namespace VNLib.Utils.Extensions /// </returns> //Method only exists for consistancy since unsafe handles are always 32bit [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetIntLength<T>(this in UnsafeMemoryHandle<T> handle) where T : unmanaged => handle.IntLength; + public static int GetIntLength<T>(this in UnsafeMemoryHandle<T> handle) where T : unmanaged + => handle.IntLength; /// <summary> /// Gets an offset pointer from the base postion to the number of bytes specified. Performs bounds checks @@ -295,6 +268,23 @@ namespace VNLib.Utils.Extensions } /// <summary> + /// Gets a reference to the element at the specified offset from the base + /// address of the <see cref="MemoryHandle{T}"/> and casts it to a byte reference + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="block"></param> + /// <param name="offset">The number of elements to offset the base reference by</param> + /// <returns>The reinterpreted byte reference at the first byte of the element offset</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref byte GetOffsetByteRef<T>(this IMemoryHandle<T> block, nint offset) + { + ArgumentOutOfRangeException.ThrowIfNegative(offset); + return ref GetOffsetByteRef(block, (nuint)offset); + } + + /// <summary> /// Gets a 64bit friendly span offset for the current <see cref="MemoryHandle{T}"/> /// </summary> /// <typeparam name="T"></typeparam> @@ -440,16 +430,8 @@ namespace VNLib.Utils.Extensions /// <exception cref="ArgumentException"></exception> /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public static unsafe MemoryHandle<T> Alloc<T>(this IUnmangedHeap heap, nuint elements, bool zero = false) where T : unmanaged - { - ArgumentNullException.ThrowIfNull(heap); - //Minimum of one element - elements = Math.Max(elements, 1); - //If zero flag is set then specify zeroing memory - IntPtr block = heap.Alloc(elements, (nuint)sizeof(T), zero); - //Return handle wrapper - return new MemoryHandle<T>(heap, block, elements, zero); - } + public static unsafe MemoryHandle<T> Alloc<T>(this IUnmangedHeap heap, nuint elements, bool zero = false) where T : unmanaged + => MemoryUtil.SafeAlloc<T>(heap, elements, zero); /// <summary> /// Allocates a block of unmanaged memory of the number of elements to store of an unmanged type @@ -464,10 +446,7 @@ namespace VNLib.Utils.Extensions /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MemoryHandle<T> Alloc<T>(this IUnmangedHeap heap, nint elements, bool zero = false) where T : unmanaged - { - ArgumentOutOfRangeException.ThrowIfNegative(elements); - return Alloc<T>(heap, (nuint)elements, zero); - } + => MemoryUtil.SafeAlloc<T>(heap, elements, zero); /// <summary> /// Allocates a buffer from the current heap and initialzies it by copying the initial data buffer @@ -481,10 +460,8 @@ namespace VNLib.Utils.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MemoryHandle<T> AllocAndCopy<T>(this IUnmangedHeap heap, ReadOnlySpan<T> initialData) where T : unmanaged { - //Aloc block - MemoryHandle<T> handle = heap.Alloc<T>(initialData.Length); - - //Copy initial data + MemoryHandle<T> handle = Alloc<T>(heap, initialData.Length); + MemoryUtil.Copy(initialData, 0, handle, 0, initialData.Length); return handle; @@ -502,10 +479,8 @@ namespace VNLib.Utils.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MemoryHandle<T> AllocAndCopy<T>(this IUnmangedHeap heap, ReadOnlyMemory<T> initialData) where T : unmanaged { - //Aloc block - MemoryHandle<T> handle = heap.Alloc<T>(initialData.Length); - - //Copy initial data + MemoryHandle<T> handle = Alloc<T>(heap, initialData.Length); + MemoryUtil.Copy(initialData, 0, handle, 0, initialData.Length); return handle; @@ -542,25 +517,8 @@ namespace VNLib.Utils.Extensions /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="ObjectDisposedException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static UnsafeMemoryHandle<T> UnsafeAlloc<T>(this IUnmangedHeap heap, int elements, bool zero = false) where T : unmanaged - { - ArgumentNullException.ThrowIfNull(heap); - - if (elements < 1) - { - //Return an empty handle - return new UnsafeMemoryHandle<T>(); - } - - //Get element size - nuint elementSize = (nuint)Unsafe.SizeOf<T>(); - - //If zero flag is set then specify zeroing memory (safe case because of the above check) - IntPtr block = heap.Alloc((nuint)elements, elementSize, zero); - - //handle wrapper - return new (heap, block, elements); - } + public unsafe static UnsafeMemoryHandle<T> UnsafeAlloc<T>(this IUnmangedHeap heap, int elements, bool zero = false) where T : unmanaged + => MemoryUtil.UnsafeAlloc<T>(heap, elements, zero); #region VnBufferWriter @@ -775,13 +733,6 @@ namespace VNLib.Utils.Extensions } /// <summary> - /// Converts the buffer data to a <see cref="PrivateString"/> - /// </summary> - /// <returns>A <see cref="PrivateString"/> instance that owns the underlying string memory</returns> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PrivateString ToPrivate(this ref ForwardOnlyWriter<char> buffer) => new(buffer.ToString(), true); - - /// <summary> /// Gets a <see cref="Span{T}"/> over the modified section of the internal buffer /// </summary> /// <returns>A <see cref="Span{T}"/> over the modified data</returns> diff --git a/lib/Utils/src/Extensions/ThreadingExtensions.cs b/lib/Utils/src/Extensions/ThreadingExtensions.cs index a80a0ae..c29ab63 100644 --- a/lib/Utils/src/Extensions/ThreadingExtensions.cs +++ b/lib/Utils/src/Extensions/ThreadingExtensions.cs @@ -38,20 +38,6 @@ namespace VNLib.Utils.Extensions public static class ThreadingExtensions { /// <summary> - /// Allows an <see cref="OpenResourceHandle{TResource}"/> to execute within a scope limited context - /// </summary> - /// <typeparam name="TResource">The resource type</typeparam> - /// <param name="rh"></param> - /// <param name="safeCallback">The function body that will execute with controlled access to the resource</param> - public static void EnterSafeContext<TResource>(this OpenResourceHandle<TResource> rh, Action<TResource> safeCallback) - { - using (rh) - { - safeCallback(rh.Resource); - } - } - - /// <summary> /// Waits for exlcusive access to the resource identified by the given moniker /// and returns a handle that will release the lock when disposed. /// </summary> @@ -106,6 +92,7 @@ namespace VNLib.Utils.Extensions await semaphore.WaitAsync(cancellationToken); return new SemSlimReleaser(semaphore); } + /// <summary> /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/> using a 32-bit signed integer to measure the time intervale /// and getting a releaser handle @@ -135,6 +122,7 @@ namespace VNLib.Utils.Extensions semaphore.Wait(); return new SemSlimReleaser(semaphore); } + /// <summary> /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/> /// </summary> @@ -164,6 +152,7 @@ namespace VNLib.Utils.Extensions mutex.WaitOne(); return new MutexReleaser(mutex); } + /// <summary> /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/> /// </summary> @@ -201,6 +190,7 @@ namespace VNLib.Utils.Extensions public static Task<bool> WaitAsync(this WaitHandle handle, int timeoutMs = Timeout.Infinite) { ArgumentNullException.ThrowIfNull(handle); + //test non-blocking handle state if (handle.WaitOne(0)) { @@ -223,13 +213,64 @@ namespace VNLib.Utils.Extensions return TrueCompleted; } } + + return NoSpinWaitAsync(handle, timeoutMs); + } + + /// <summary> + /// Asynchronously waits for a the <see cref="WaitHandle"/> to receive a signal. This method spins until + /// a thread yield will occur, then asynchronously yields. + /// </summary> + /// <param name="handle"></param> + /// <param name="timeoutMs">The timeout interval in milliseconds</param> + /// <param name="cancellation">A <see cref="CancellationToken"/> used to cancel the asynct wait event</param> + /// <returns> + /// A task that compeletes when the wait handle receives a signal or times-out, + /// the result of the awaited task will be <c>true</c> if the signal is received, or + /// <c>false</c> if the timeout interval expires + /// </returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static Task<bool> WaitAsync(this WaitHandle handle, int timeoutMs, CancellationToken cancellation = default) + { + Task<bool> withoutToken = WaitAsync(handle, timeoutMs); + + return withoutToken.IsCompleted + ? withoutToken + : withoutToken.WaitAsync(cancellation); + } + + /// <summary> + /// Asynchronously waits for a the <see cref="WaitHandle"/> to receive a signal, without checking + /// current state or spinning. This function always returns a new task that will complete when the + /// handle is signaled or the timeout interval expires. + /// </summary> + /// <param name="handle"></param> + /// <param name="timeoutMs">Time (in ms)</param> + /// <returns></returns> + public static Task<bool> NoSpinWaitAsync(this WaitHandle handle, int timeoutMs) + { //Completion source used to signal the awaiter when the wait handle is signaled TaskCompletionSource<bool> completion = new(TaskCreationOptions.None); + //Register wait on threadpool to complete the task source - RegisteredWaitHandle registration = ThreadPool.RegisterWaitForSingleObject(handle, TaskCompletionCallback, completion, timeoutMs, true); + RegisteredWaitHandle registration = ThreadPool.RegisterWaitForSingleObject( + handle, + TaskCompletionCallback, + completion, + timeoutMs, executeOnlyOnce: true + ); + //Register continuation to cleanup - _ = completion.Task.ContinueWith(CleanupContinuation, registration, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default) - .ConfigureAwait(false); + _ = completion.Task.ContinueWith( + CleanupContinuation, + registration, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default + ).ConfigureAwait(false); + return completion.Task; } diff --git a/lib/Utils/src/IO/ArrayPoolStreamBuffer.cs b/lib/Utils/src/IO/ArrayPoolStreamBuffer.cs index b62412f..a943a5b 100644 --- a/lib/Utils/src/IO/ArrayPoolStreamBuffer.cs +++ b/lib/Utils/src/IO/ArrayPoolStreamBuffer.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Utils @@ -28,7 +28,13 @@ using System.Buffers; namespace VNLib.Utils.IO { - internal class ArrayPoolStreamBuffer<T> : ISlindingWindowBuffer<T> + /// <summary> + /// Creates a new <see cref="ArrayPoolStreamBuffer{T}"/> from the + /// given array instance and <see cref="ArrayPool{T}"/> it came from. + /// </summary> + /// <param name="array">The rented array to use</param> + /// <param name="pool">The pool to return the array to when completed</param> + internal class ArrayPoolStreamBuffer<T>(T[] array, ArrayPool<T> pool) : ISlindingWindowBuffer<T> { /// <summary> /// The shared <see cref="IStreamBufferFactory{T}"/> instance to allocate buffers @@ -36,20 +42,8 @@ namespace VNLib.Utils.IO /// </summary> public static IStreamBufferFactory<T> Shared { get; } = new DefaultFactory(); - private readonly ArrayPool<T> _pool; - private T[] _buffer; - - /// <summary> - /// Creates a new <see cref="ArrayPoolStreamBuffer{T}"/> from the - /// given array instance and <see cref="ArrayPool{T}"/> it came from. - /// </summary> - /// <param name="array">The rented array to use</param> - /// <param name="pool">The pool to return the array to when completed</param> - public ArrayPoolStreamBuffer(T[] array, ArrayPool<T> pool) - { - _pool = pool; - _buffer = array; - } + private readonly ArrayPool<T> _pool = pool; + private T[] _buffer = array; ///<inheritdoc/> public int WindowStartPos { get; set; } diff --git a/lib/Utils/src/IO/IVnTextReader.cs b/lib/Utils/src/IO/IVnTextReader.cs index 625ba78..93de2d1 100644 --- a/lib/Utils/src/IO/IVnTextReader.cs +++ b/lib/Utils/src/IO/IVnTextReader.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Utils @@ -23,7 +23,6 @@ */ using System; -using System.IO; using System.Text; namespace VNLib.Utils.IO @@ -34,10 +33,6 @@ namespace VNLib.Utils.IO public interface IVnTextReader { /// <summary> - /// The base stream to read data from - /// </summary> - Stream BaseStream { get; } - /// <summary> /// The character encoding used by the TextReader /// </summary> Encoding Encoding { get; } diff --git a/lib/Utils/src/IO/VnMemoryStream.cs b/lib/Utils/src/IO/VnMemoryStream.cs index f4bf970..3f14061 100644 --- a/lib/Utils/src/IO/VnMemoryStream.cs +++ b/lib/Utils/src/IO/VnMemoryStream.cs @@ -41,15 +41,21 @@ namespace VNLib.Utils.IO /// </summary> public sealed class VnMemoryStream : Stream, ICloneable { + public const int DefaultBufferSize = 4096; + private nint _position; private nint _length; private bool _isReadonly; //Memory private readonly IResizeableMemoryHandle<byte> _buffer; + //Default owns handle private readonly bool OwnsHandle = true; + //Lazy loaded memory wrapper + private MemoryManager<byte>? _memoryWrapper; + /// <summary> /// Creates a new <see cref="VnMemoryStream"/> pointing to the begining of memory, and consumes the handle. /// </summary> @@ -78,15 +84,19 @@ namespace VNLib.Utils.IO ArgumentNullException.ThrowIfNull(handle); return handle.CanRealloc || readOnly - ? new VnMemoryStream(handle, length, readOnly, ownsHandle) + ? new VnMemoryStream(handle, existingManager: null, length, readOnly, ownsHandle) : throw new ArgumentException("The supplied memory handle must be resizable on a writable stream", nameof(handle)); } /// <summary> /// Converts a writable <see cref="VnMemoryStream"/> to readonly to allow shallow copies /// </summary> + /// <remarks> + /// This funciton will convert the stream passed into it to a readonly stream. + /// The function passes through the input stream as the return value + /// </remarks> /// <param name="stream">The stream to make readonly</param> - /// <returns>The readonly stream</returns> + /// <returns>A reference to the modified input stream</returns> public static VnMemoryStream CreateReadonly(VnMemoryStream stream) { ArgumentNullException.ThrowIfNull(stream); @@ -101,15 +111,21 @@ namespace VNLib.Utils.IO /// global heap instance. /// </summary> public VnMemoryStream() : this(MemoryUtil.Shared) { } - + /// <summary> /// Create a new memory stream where buffers will be allocated from the specified heap /// </summary> /// <param name="heap"><see cref="Win32PrivateHeap"/> to allocate memory from</param> /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="ArgumentNullException"></exception> - public VnMemoryStream(IUnmangedHeap heap) : this(heap, 0, false) { } - + public VnMemoryStream(IUnmangedHeap heap) : this(heap, DefaultBufferSize, false) { } + + /// <summary> + /// Creates a new memory stream using the <see cref="MemoryUtil.Shared"/> + /// global heap instance. + /// </summary> + public VnMemoryStream(nuint bufferSize, bool zero) : this(MemoryUtil.Shared, bufferSize, zero) { } + /// <summary> /// Creates a new memory stream and pre-allocates the internal /// buffer of the specified size on the specified heap to avoid resizing. @@ -163,7 +179,14 @@ namespace VNLib.Utils.IO /// <param name="length">The length property of the stream</param> /// <param name="readOnly">Is the stream readonly (should mostly be true!)</param> /// <param name="ownsHandle">Does the new stream own the memory -> <paramref name="buffer"/></param> - private VnMemoryStream(IResizeableMemoryHandle<byte> buffer, nint length, bool readOnly, bool ownsHandle) + /// <param name="existingManager">A reference to an existing memory manager class</param> + private VnMemoryStream( + IResizeableMemoryHandle<byte> buffer, + MemoryManager<byte>? existingManager, + nint length, + bool readOnly, + bool ownsHandle + ) { Debug.Assert(length >= 0, "Length must be positive"); Debug.Assert(buffer.CanRealloc || readOnly, "The supplied buffer is not resizable on a writable stream"); @@ -172,6 +195,7 @@ namespace VNLib.Utils.IO _buffer = buffer; //Consume the handle _length = length; //Store length of the buffer _isReadonly = readOnly; + _memoryWrapper = existingManager; } /// <summary> @@ -189,7 +213,7 @@ namespace VNLib.Utils.IO //Create a new readonly copy (stream does not own the handle) return !_isReadonly ? throw new NotSupportedException("This stream is not readonly. Cannot create shallow copy on a mutable stream") - : new VnMemoryStream(_buffer, _length, true, false); + : new VnMemoryStream(_buffer, _memoryWrapper, _length, readOnly: true, ownsHandle: false); } /// <summary> @@ -247,27 +271,31 @@ namespace VNLib.Utils.IO cancellationToken.ThrowIfCancellationRequested(); + //Memory manager requires 32bit or less in length if(_length < Int32.MaxValue) { - //Safe to alloc a memory manager to do copy - using MemoryManager<byte> asMemManager = _buffer.ToMemoryManager(false); + //Get/alloc the internal memory manager and get the block + ReadOnlyMemory<byte> asMemory = AsMemory(); + + Debug.Assert(asMemory.Length >= LenToPosDiff, "Internal memory block smaller than desired for stream copy"); /* * CopyTo starts at the current position, as if calling Read() * so the reader must be offset to match and the _length gives us the * actual length of the stream and therefor the segment size - */ + */ - while(LenToPosDiff > 0) + while (LenToPosDiff > 0) { int blockSize = Math.Min((int)LenToPosDiff, bufferSize); - Memory<byte> window = asMemManager.Memory.Slice((int)_position, blockSize); + + ReadOnlyMemory<byte> window = asMemory.Slice((int)_position, blockSize); //write async await destination.WriteAsync(window, cancellationToken); //Update position - _position+= bufferSize; + _position += bufferSize; } } else @@ -354,7 +382,7 @@ namespace VNLib.Utils.IO } ///<inheritdoc/> - public override unsafe int ReadByte() + public override int ReadByte() { if (LenToPosDiff == 0) { @@ -362,7 +390,7 @@ namespace VNLib.Utils.IO } //get the value at the current position - ref byte ptr = ref _buffer.GetOffsetByteRef((nuint)_position); + ref byte ptr = ref _buffer.GetOffsetByteRef(_position); //Increment position _position++; @@ -486,7 +514,7 @@ namespace VNLib.Utils.IO throw new NotSupportedException("Write operation is not allowed on readonly stream!"); } //Calculate the new final position - nint newPos = (_position + buffer.Length); + nint newPos = checked(_position + buffer.Length); //Determine if the buffer needs to be expanded if (buffer.Length > LenToPosDiff) { @@ -495,8 +523,16 @@ namespace VNLib.Utils.IO //Update length _length = newPos; } + //Copy the input buffer to the internal buffer - MemoryUtil.Copy(buffer, 0, _buffer, (nuint)_position, buffer.Length); + MemoryUtil.Copy( + source: buffer, + sourceOffset: 0, + dest: _buffer, + destOffset: (nuint)_position, + count: buffer.Length + ); + //Update the position _position = newPos; } @@ -550,6 +586,7 @@ namespace VNLib.Utils.IO /// <summary> /// Returns a <see cref="ReadOnlySpan{T}"/> window over the data within the entire stream + /// that is equal in length to the stream length. /// </summary> /// <returns>A <see cref="ReadOnlySpan{T}"/> of the data within the entire stream</returns> /// <exception cref="OverflowException"></exception> @@ -560,6 +597,40 @@ namespace VNLib.Utils.IO //Get span with no offset return _buffer.AsSpan(0, len); } + + /// <summary> + /// Returns a <see cref="ReadOnlyMemory{T}"/> structure which is a window of the buffered + /// data as it currently sits. For writeable straems, you must call this function + /// every time the size of the stream changes. The memory structure is just a "pointer" to + /// the internal buffer. + /// </summary> + /// <returns> + /// A memory snapshot of the stream. + /// </returns> + /// <remarks> + /// This function causes an internal allocation on the first call. After the first call + /// to this function, all calls are thread-safe. + /// </remarks> + public ReadOnlyMemory<byte> AsMemory() + { + /* + * Safe cast stram length to int, because memory window requires a 32bit + * integer. Also will throw before allocating the mmemory manager + */ + + int len = Convert.ToInt32(_length); + + //Defer/lazy init the memory manager + MemoryManager<byte> asMemory = LazyInitializer.EnsureInitialized(ref _memoryWrapper, AllocMemManager); + + Debug.Assert(asMemory != null); + + /* + * Buffer window may be larger than the actual stream legnth, so + * slice the memory to the actual length of the stream + */ + return asMemory.Memory[..len]; + } /// <summary> /// If the current stream is a readonly stream, creates a shallow copy for reading only. @@ -568,5 +639,7 @@ namespace VNLib.Utils.IO /// <exception cref="NotSupportedException"></exception> public object Clone() => GetReadonlyShallowCopy(); + private MemoryManager<byte> AllocMemManager() => _buffer.ToMemoryManager(false); + } -}
\ No newline at end of file +} diff --git a/lib/Utils/src/IO/VnStreamWriter.cs b/lib/Utils/src/IO/VnStreamWriter.cs index ddebc07..7d43f48 100644 --- a/lib/Utils/src/IO/VnStreamWriter.cs +++ b/lib/Utils/src/IO/VnStreamWriter.cs @@ -41,21 +41,29 @@ namespace VNLib.Utils.IO /// Provides a memory optimized <see cref="TextWriter"/> implementation. Optimized for writing /// to network streams /// </summary> - public class VnStreamWriter : TextWriter + /// <remarks> + /// Creates a new <see cref="VnStreamWriter"/> that writes encoded data to the base stream + /// and uses the specified buffer. + /// </remarks> + /// <param name="baseStream">The underlying stream to write data to</param> + /// <param name="encoding">The <see cref="Encoding"/> to use when writing to the stream</param> + /// <param name="buffer">The internal <see cref="ISlindingWindowBuffer{T}"/> to use</param> + /// <exception cref="ArgumentNullException"></exception> + public class VnStreamWriter(Stream baseStream, Encoding encoding, ISlindingWindowBuffer<byte> buffer) : TextWriter { - private readonly Encoder Enc; + private readonly Encoder Enc = encoding.GetEncoder(); - private readonly ISlindingWindowBuffer<byte> _buffer; + private readonly ISlindingWindowBuffer<byte> _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); private bool closed; /// <summary> /// Gets the underlying stream that interfaces with the backing store /// </summary> - public virtual Stream BaseStream { get; } + public virtual Stream BaseStream { get; } = baseStream ?? throw new ArgumentNullException(nameof(buffer)); ///<inheritdoc/> - public override Encoding Encoding { get; } + public override Encoding Encoding { get; } = encoding ?? throw new ArgumentNullException(nameof(encoding)); /// <summary> /// Line termination to use when writing lines to the output @@ -97,24 +105,6 @@ namespace VNLib.Utils.IO { } - /// <summary> - /// Creates a new <see cref="VnStreamWriter"/> that writes encoded data to the base stream - /// and uses the specified buffer. - /// </summary> - /// <param name="baseStream">The underlying stream to write data to</param> - /// <param name="encoding">The <see cref="Encoding"/> to use when writing to the stream</param> - /// <param name="buffer">The internal <see cref="ISlindingWindowBuffer{T}"/> to use</param> - /// <exception cref="ArgumentNullException"></exception> - public VnStreamWriter(Stream baseStream, Encoding encoding, ISlindingWindowBuffer<byte> buffer) - { - BaseStream = baseStream ?? throw new ArgumentNullException(nameof(buffer)); - Encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); - _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); - - //Get an encoder - Enc = encoding.GetEncoder(); - } - ///<inheritdoc/> public void Write(byte value) { diff --git a/lib/Utils/src/IO/VnTextReaderExtensions.cs b/lib/Utils/src/IO/VnTextReaderExtensions.cs index 9ca5ae5..5dc6117 100644 --- a/lib/Utils/src/IO/VnTextReaderExtensions.cs +++ b/lib/Utils/src/IO/VnTextReaderExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Utils @@ -66,6 +66,7 @@ namespace VNLib.Utils.IO /// <remarks>Allows reading lines of data from the stream without allocations</remarks> public static ERRNO ReadLine<T>(this T reader, Span<char> charBuffer) where T : class, IVnTextReader { + ArgumentNullException.ThrowIfNull(reader); return ReadLineInternal(ref reader, charBuffer); } @@ -118,6 +119,7 @@ namespace VNLib.Utils.IO /// <remarks>You should use the <see cref="IVnTextReader.Available"/> property to know how much remaining data is buffered</remarks> public static int ReadRemaining<T>(this T reader, Span<byte> buffer) where T : class, IVnTextReader { + ArgumentNullException.ThrowIfNull(reader); return ReadRemainingInternal(ref reader, buffer); } diff --git a/lib/Utils/src/Memory/MemoryHandle.cs b/lib/Utils/src/Memory/MemoryHandle.cs index 16fc555..fbaae95 100644 --- a/lib/Utils/src/Memory/MemoryHandle.cs +++ b/lib/Utils/src/Memory/MemoryHandle.cs @@ -229,8 +229,7 @@ namespace VNLib.Utils.Memory //If adding ref failed, the handle is closed ObjectDisposedException.ThrowIf(!addRef, this); - - //Create a new system.buffers memory handle from the offset ptr address + return new MemoryHandle(ptr, pinnable: this); } @@ -250,7 +249,10 @@ namespace VNLib.Utils.Memory /// <exception cref="ObjectDisposedException"></exception> public bool Equals(MemoryHandle<T>? other) { - return other != null && (IsClosed | other.IsClosed) == false && _length == other._length && handle == other.handle; + return other != null + && (IsClosed | other.IsClosed) == false + && _length == other._length + && handle == other.handle; } ///<inheritdoc/> diff --git a/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs b/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs index 9decef7..6899877 100644 --- a/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs +++ b/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs @@ -308,4 +308,4 @@ namespace VNLib.Utils.Memory } } } -}
\ No newline at end of file +} diff --git a/lib/Utils/src/Memory/MemoryUtil.cs b/lib/Utils/src/Memory/MemoryUtil.cs index 7ce7c81..09c36d5 100644 --- a/lib/Utils/src/Memory/MemoryUtil.cs +++ b/lib/Utils/src/Memory/MemoryUtil.cs @@ -143,7 +143,8 @@ namespace VNLib.Utils.Memory * get the heap's stats, otherwise return an empty handle */ return _lazyHeap.IsLoaded && _lazyHeap.Instance is TrackedHeapWrapper h - ? h.GetCurrentStats() : default; + ? h.GetCurrentStats() + : default; } /// <summary> @@ -518,7 +519,8 @@ namespace VNLib.Utils.Memory /// <param name="target">A pointer to initialized target structure to copy data to</param> /// <exception cref="ArgumentNullException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CopyStruct<T>(ref readonly byte source, void* target) where T : unmanaged => CopyStruct(in source, (T*)target); + public static void CopyStruct<T>(ref readonly byte source, void* target) where T : unmanaged + => CopyStruct(in source, (T*)target); /// <summary> /// Copies structure data from a source sequence of data to the target structure reference. @@ -547,7 +549,8 @@ namespace VNLib.Utils.Memory /// <param name="target">A pointer to initialized target structure to copy data to</param> /// <exception cref="ArgumentNullException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CopyStruct<T>(ReadOnlySpan<byte> sourceData, T* target) where T : unmanaged => CopyStruct(sourceData, ref *target); + public static void CopyStruct<T>(ReadOnlySpan<byte> sourceData, T* target) where T : unmanaged + => CopyStruct(sourceData, ref *target); /// <summary> /// Copies structure data from a source sequence of data to the target structure reference. @@ -557,7 +560,8 @@ namespace VNLib.Utils.Memory /// <param name="target">A pointer to initialized target structure to copy data to</param> /// <exception cref="ArgumentNullException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CopyStruct<T>(ReadOnlySpan<byte> sourceData, void* target) where T: unmanaged => CopyStruct(sourceData, (T*)target); + public static void CopyStruct<T>(ReadOnlySpan<byte> sourceData, void* target) where T: unmanaged + => CopyStruct(sourceData, (T*)target); /// <summary> @@ -742,7 +746,7 @@ namespace VNLib.Utils.Memory ref Refs.AsByte(source, (nuint)sourceOffset), ref Refs.AsByte(dest, destOffset), ByteCount<T>((uint)count), - false + forceAcceleration: false ); } @@ -773,7 +777,7 @@ namespace VNLib.Utils.Memory RMemCopyHandle<T> src = new(source, (nuint)sourceOffset); MemhandleCopyHandle<T> dst = new(dest, destOffset); - MemmoveInternal<T, RMemCopyHandle<T>, MemhandleCopyHandle<T>>(in src, in dst, (nuint)count, false); + MemmoveInternal<T, RMemCopyHandle<T>, MemhandleCopyHandle<T>>(in src, in dst, (nuint)count, forceAcceleration: false); } /// <summary> @@ -808,7 +812,7 @@ namespace VNLib.Utils.Memory ref Refs.AsByte(source, (nuint)sourceOffset), ref Refs.AsByte(dest, (nuint)destOffset), ByteCount<T>((uint)count), - false + forceAcceleration: false ); } @@ -841,7 +845,7 @@ namespace VNLib.Utils.Memory MemhandleCopyHandle<T> src = new(source, (nuint)sourceOffset); WMemCopyHandle<T> dst = new(dest, (nuint)destOffset); - MemmoveInternal<T, MemhandleCopyHandle<T>, WMemCopyHandle<T>>(in src, in dst, (nuint)count, false); + MemmoveInternal<T, MemhandleCopyHandle<T>, WMemCopyHandle<T>>(in src, in dst, (nuint)count, forceAcceleration: false); } /// <summary> @@ -871,7 +875,7 @@ namespace VNLib.Utils.Memory MemhandleCopyHandle<T> src = new(source, sourceOffset); MemhandleCopyHandle<T> dst = new(dest, destOffset); - MemmoveInternal<T, MemhandleCopyHandle<T>, MemhandleCopyHandle<T>>(in src, in dst, count, false); + MemmoveInternal<T, MemhandleCopyHandle<T>, MemhandleCopyHandle<T>>(in src, in dst, count, forceAcceleration: false); } /// <summary> @@ -900,7 +904,7 @@ namespace VNLib.Utils.Memory MemhandleCopyHandle<T> src = new(source, sourceOffset); ArrayCopyHandle<T> dst = new(dest, destOffset); - MemmoveInternal<T, MemhandleCopyHandle<T>, ArrayCopyHandle<T>>(in src, in dst, count, false); + MemmoveInternal<T, MemhandleCopyHandle<T>, ArrayCopyHandle<T>>(in src, in dst, count, forceAcceleration: false); } /// <summary> @@ -929,7 +933,7 @@ namespace VNLib.Utils.Memory ArrayCopyHandle<T> ach = new(source, sourceOffset); MemhandleCopyHandle<T> mch = new(dest, destOffset); - MemmoveInternal<T, ArrayCopyHandle<T>, MemhandleCopyHandle<T>>(in ach, in mch, count, false); + MemmoveInternal<T, ArrayCopyHandle<T>, MemhandleCopyHandle<T>>(in ach, in mch, count, forceAcceleration: false); } /// <summary> @@ -958,13 +962,14 @@ namespace VNLib.Utils.Memory ArrayCopyHandle<T> srcH = new(source, sourceOffset); ArrayCopyHandle<T> dstH = new(dest, destOffset); - MemmoveInternal<T, ArrayCopyHandle<T>, ArrayCopyHandle<T>>(in srcH, in dstH, count, false); + MemmoveInternal<T, ArrayCopyHandle<T>, ArrayCopyHandle<T>>(in srcH, in dstH, count, forceAcceleration: false); } /// <summary> /// Optimized memmove for known small memory blocks. This method is faster than /// <see cref="Memmove{T}(ref readonly T, nuint, ref T, nuint, nuint)"/> when the - /// number of elements to copy is known to be small. + /// number of elements to copy is known to be small. Pointers to src and dst may be + /// overlapping regions of memory. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="src">A readonly reference to the first element in the source memory sequence</param> @@ -999,7 +1004,7 @@ namespace VNLib.Utils.Memory /// <summary> /// Low level api for copying data from source memory to destination memory of an - /// umanged data type. + /// umanged data type. Pointers to src and dst may be overlapping regions of memory. /// </summary> /// <remarks> /// WARNING: It's not possible to do bounds checking when using references. Be sure you @@ -1027,13 +1032,14 @@ namespace VNLib.Utils.Memory in Refs.AsByteR(in src, srcOffset), ref Refs.AsByte(ref dst, dstOffset), ByteCount<T>(elementCount), - false + forceAcceleration: false ); } /// <summary> /// Low level api for copying data from source memory to destination memory of an /// umanged data type. This call attempts to force hadrware acceleration if supported. + /// Pointers to src and dst may be overlapping regions of memory. /// <para> /// Understand that using this function attempts to force hardware acceleration, which /// may hurt performance if the data is not large enough to justify the overhead. @@ -1329,7 +1335,7 @@ namespace VNLib.Utils.Memory //Pin the array GCHandle arrHandle = GCHandle.Alloc(array, GCHandleType.Pinned); - //safe to get array basee pointer + //safe to get array base pointer ref T arrBase = ref MemoryMarshal.GetArrayDataReference(array); //Get element offset @@ -1571,8 +1577,6 @@ namespace VNLib.Utils.Memory MemoryHandle Pin(); - nuint Size { get; } - nuint Offset { get; } void Validate(nuint count); @@ -1584,9 +1588,6 @@ namespace VNLib.Utils.Memory public readonly nuint Offset => offset; ///<inheritdoc/> - public readonly nuint Size => ByteCount<T>((nuint)array.Length); - - ///<inheritdoc/> public readonly MemoryHandle Pin() => PinArrayAndGetHandle(array, 0); ///<inheritdoc/> @@ -1602,9 +1603,6 @@ namespace VNLib.Utils.Memory public readonly nuint Offset => offset; ///<inheritdoc/> - public readonly nuint Size => ByteCount<T>((nuint)block.Length); - - ///<inheritdoc/> public readonly MemoryHandle Pin() => block.Pin(); ///<inheritdoc/> @@ -1620,9 +1618,6 @@ namespace VNLib.Utils.Memory public readonly nuint Offset => offset; ///<inheritdoc/> - public readonly nuint Size => ByteCount<T>((nuint)block.Length); - - ///<inheritdoc/> public readonly MemoryHandle Pin() => block.Pin(); ///<inheritdoc/> @@ -1638,9 +1633,6 @@ namespace VNLib.Utils.Memory public readonly nuint Offset => offset; ///<inheritdoc/> - public readonly nuint Size => handle.Length; - - ///<inheritdoc/> public readonly MemoryHandle Pin() => handle.Pin(0); ///<inheritdoc/> diff --git a/lib/Utils/src/Memory/MemoryUtilAlloc.cs b/lib/Utils/src/Memory/MemoryUtilAlloc.cs index e2e7434..742346c 100644 --- a/lib/Utils/src/Memory/MemoryUtilAlloc.cs +++ b/lib/Utils/src/Memory/MemoryUtilAlloc.cs @@ -27,14 +27,83 @@ using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; -using VNLib.Utils.Extensions; - namespace VNLib.Utils.Memory { public static unsafe partial class MemoryUtil { #region alloc + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanUseUnmanagedHeap<T>(IUnmangedHeap heap, nuint elements) + { + /* + * We may allocate from the share heap only if the heap is not using locks + * or if the element size could cause performance issues because its too large + * to use a managed array. + * + * We want to avoid allocations, that may end up in the LOH if we can + */ + + return (heap.CreationFlags & HeapCreation.UseSynchronization) == 0 + || ByteCount<T>((uint)elements) > MAX_UNSAFE_POOL_SIZE; + } + + /// <summary> + /// Allocates a block of unmanaged memory of the number of elements to store of an unmanged type + /// </summary> + /// <typeparam name="T">Unmanaged data type to create a block of</typeparam> + /// <param name="heap"></param> + /// <param name="elements">The size of the block (number of elements)</param> + /// <param name="zero">A flag that zeros the allocated block before returned</param> + /// <returns>The unmanaged <see cref="MemoryHandle{T}"/></returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public static UnsafeMemoryHandle<T> UnsafeAlloc<T>(IUnmangedHeap heap, int elements, bool zero = false) where T : unmanaged + { + ArgumentNullException.ThrowIfNull(heap); + ArgumentOutOfRangeException.ThrowIfNegative(elements); + + if (elements == 0) + { + //Return an empty handle + return default; + } + + //If zero flag is set then specify zeroing memory (safe case because of the above check) + IntPtr block = heap.Alloc((nuint)elements, (nuint)sizeof(T), zero); + + return new(heap, block, elements); + } + + /// <summary> + /// Rents a new array and stores it as a resource within an <see cref="OpenResourceHandle{T}"/> to return the + /// array when work is completed + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="pool"></param> + /// <param name="size">The minimum size array to allocate</param> + /// <param name="zero">Should elements from 0 to size be set to default(T)</param> + /// <returns>A new <see cref="OpenResourceHandle{T}"/> encapsulating the rented array</returns> + public static UnsafeMemoryHandle<T> UnsafeAlloc<T>(ArrayPool<T> pool, int size, bool zero = false) where T : unmanaged + { + ArgumentNullException.ThrowIfNull(pool); + + if (size <= 0) + { + return default; + } + + T[] array = pool.Rent(size); + + if (zero) + { + InitializeBlock(array, (uint)size); + } + + return new(pool, array, size); + } + /// <summary> /// Allocates a block of unmanaged, or pooled manaaged memory depending on /// compilation flags and runtime unamanged allocators. @@ -43,40 +112,20 @@ namespace VNLib.Utils.Memory /// <param name="elements">The number of elements of the type within the block</param> /// <param name="zero">Flag to zero elements during allocation before the method returns</param> /// <returns>A handle to the block of memory</returns> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="OutOfMemoryException"></exception> public static UnsafeMemoryHandle<T> UnsafeAlloc<T>(int elements, bool zero = false) where T : unmanaged { - if (elements < 0) - { - throw new ArgumentException("Number of elements must be a positive integer", nameof(elements)); - } + ArgumentOutOfRangeException.ThrowIfNegative(elements); if (elements == 0) { return default; } - /* - * We may allocate from the share heap only if the heap is not using locks - * or if the element size could cause performance issues because its too large - * to use a managed array. - * - * We want to avoid allocations, that may end up in the LOH if we can - */ - - if ((Shared.CreationFlags & HeapCreation.UseSynchronization) == 0 || ByteCount<T>((uint)elements) > MAX_UNSAFE_POOL_SIZE) - { - // Alloc from heap - IntPtr block = Shared.Alloc((uint)elements, (uint)sizeof(T), zero); - //Init new handle - return new(Shared, block, elements); - } - else - { - //Rent the array from the pool - return ArrayPool<T>.Shared.UnsafeAlloc(elements, zero); - } + return CanUseUnmanagedHeap<T>(Shared, (uint)elements) + ? UnsafeAlloc<T>(Shared, elements, zero) + : UnsafeAlloc(ArrayPool<T>.Shared, elements, zero); } /// <summary> @@ -88,18 +137,81 @@ namespace VNLib.Utils.Memory /// <param name="elements">The number of elements of the type within the block</param> /// <param name="zero">Flag to zero elements during allocation before the method returns</param> /// <returns>A handle to the block of memory</returns> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="OutOfMemoryException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static UnsafeMemoryHandle<T> UnsafeAllocNearestPage<T>(int elements, bool zero = false) where T : unmanaged { - if (elements < 0) + ArgumentOutOfRangeException.ThrowIfNegative(elements); + return UnsafeAlloc<T>(elements: (int)NearestPage<T>(elements), zero); + } + + /// <summary> + /// Allocates a block of unmanaged memory of the number of elements to store of an unmanged type + /// </summary> + /// <typeparam name="T">Unmanaged data type to create a block of</typeparam> + /// <param name="heap"></param> + /// <param name="elements">The size of the block (number of elements)</param> + /// <param name="zero">A flag that zeros the allocated block before returned</param> + /// <returns>The unmanaged <see cref="MemoryHandle{T}"/></returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public static MemoryHandle<T> SafeAlloc<T>(IUnmangedHeap heap, nuint elements, bool zero = false) where T : unmanaged + { + ArgumentNullException.ThrowIfNull(heap); + + //Return empty handle if no elements were specified + if (elements == 0) { - throw new ArgumentException("Number of elements must be a positive integer", nameof(elements)); + return new MemoryHandle<T>(); } + + IntPtr block = heap.Alloc(elements, (nuint)sizeof(T), zero); - //Round to nearest page (in bytes) - nint np = NearestPage<T>(elements); - return UnsafeAlloc<T>((int)np, zero); + return new MemoryHandle<T>(heap, block, elements, zero); + } + + /// <summary> + /// Allocates a block of unmanaged memory of the number of elements to store of an unmanged type + /// </summary> + /// <typeparam name="T">Unmanaged data type to create a block of</typeparam> + /// <param name="heap"></param> + /// <param name="elements">The size of the block (number of elements)</param> + /// <param name="zero">A flag that zeros the allocated block before returned</param> + /// <returns>The unmanaged <see cref="MemoryHandle{T}"/></returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryHandle<T> SafeAlloc<T>(IUnmangedHeap heap, nint elements, bool zero = false) where T : unmanaged + { + ArgumentOutOfRangeException.ThrowIfNegative(elements); + return SafeAlloc<T>(heap, (nuint)elements, zero); + } + + /// <summary> + /// Rents a new array and stores it as a resource within an <see cref="OpenResourceHandle{T}"/> to return the + /// array when work is completed + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="pool"></param> + /// <param name="size">The minimum size array to allocate</param> + /// <param name="zero">Should elements from 0 to size be set to default(T)</param> + /// <returns>A new <see cref="OpenResourceHandle{T}"/> encapsulating the rented array</returns> + public static ArrayPoolBuffer<T> SafeAlloc<T>(ArrayPool<T> pool, int size, bool zero = false) where T : struct + { + ArgumentNullException.ThrowIfNull(pool); + + T[] array = pool.Rent(size); + + if (zero) + { + InitializeBlock(array, (uint)size); + } + + //Use the array pool buffer wrapper to return the array to the pool when the handle is disposed + return new ArrayPoolBuffer<T>(pool, array, size); } /// <summary> @@ -110,35 +222,60 @@ namespace VNLib.Utils.Memory /// <param name="elements">The number of elements of the type within the block</param> /// <param name="zero">Flag to zero elements during allocation before the method returns</param> /// <returns>A handle to the block of memory</returns> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="OutOfMemoryException"></exception> - public static IMemoryHandle<T> SafeAlloc<T>(int elements, bool zero = false) where T : unmanaged + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IMemoryHandle<T> SafeAlloc<T>(nuint elements, bool zero = false) where T : unmanaged { - if (elements < 0) - { - throw new ArgumentException("Number of elements must be a positive integer", nameof(elements)); - } - - /* - * We may allocate from the share heap only if the heap is not using locks - * or if the element size could cause performance issues because its too large - * to use a managed array. - * - * We want to avoid allocations, that may end up in the LOH if we can - */ + ArgumentOutOfRangeException.ThrowIfNegative(elements); - if ((Shared.CreationFlags & HeapCreation.UseSynchronization) == 0 || ByteCount<T>((uint)elements) > MAX_UNSAFE_POOL_SIZE) + if (CanUseUnmanagedHeap<T>(Shared, elements)) { - return Shared.Alloc<T>(elements, zero); + return SafeAlloc<T>(Shared, elements, zero); } else { - return new ArrayPoolBuffer<T>(ArrayPool<T>.Shared, elements, zero); + //Should never happen because max pool size guards against this + Debug.Assert(elements <= int.MaxValue); + + return SafeAlloc(ArrayPool<T>.Shared, (int)elements, zero); } } /// <summary> /// Allocates a block of unmanaged, or pooled manaaged memory depending on + /// compilation flags and runtime unamanged allocators. + /// </summary> + /// <typeparam name="T">The unamanged type to allocate</typeparam> + /// <param name="elements">The number of elements of the type within the block</param> + /// <param name="zero">Flag to zero elements during allocation before the method returns</param> + /// <returns>A handle to the block of memory</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IMemoryHandle<T> SafeAlloc<T>(int elements, bool zero = false) where T : unmanaged + { + ArgumentOutOfRangeException.ThrowIfNegative(elements); + return SafeAlloc<T>((nuint)elements, zero); + } + + /// <summary> + /// Allocates a block of unmanaged, or pooled manaaged memory depending on + /// compilation flags and runtime unamanged allocators, rounded up to the + /// neareset memory page. + /// </summary> + /// <typeparam name="T">The unamanged type to allocate</typeparam> + /// <param name="elements">The number of elements of the type within the block</param> + /// <param name="zero">Flag to zero elements during allocation before the method returns</param> + /// <returns>A handle to the block of memory</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IMemoryHandle<T> SafeAllocNearestPage<T>(nuint elements, bool zero = false) where T : unmanaged + => SafeAlloc<T>(elements: NearestPage<T>(elements), zero); + + /// <summary> + /// Allocates a block of unmanaged, or pooled manaaged memory depending on /// compilation flags and runtime unamanged allocators, rounded up to the /// neareset memory page. /// </summary> @@ -146,21 +283,52 @@ namespace VNLib.Utils.Memory /// <param name="elements">The number of elements of the type within the block</param> /// <param name="zero">Flag to zero elements during allocation before the method returns</param> /// <returns>A handle to the block of memory</returns> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="OutOfMemoryException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IMemoryHandle<T> SafeAllocNearestPage<T>(int elements, bool zero = false) where T : unmanaged { - if (elements < 0) - { - throw new ArgumentException("Number of elements must be a positive integer", nameof(elements)); - } + ArgumentOutOfRangeException.ThrowIfNegative(elements); + return SafeAllocNearestPage<T>((nuint)elements, zero); + } - //Round to nearest page (in bytes) - nint np = NearestPage<T>(elements); - return SafeAlloc<T>((int)np, zero); + /// <summary> + /// Allocates a block of unmanaged, or pooled manaaged memory depending on + /// compilation flags and runtime unamanged allocators, rounded up to the + /// neareset memory page. + /// </summary> + /// <typeparam name="T">The unamanged type to allocate</typeparam> + /// <param name="elements">The number of elements of the type within the block</param> + /// <param name="zero">Flag to zero elements during allocation before the method returns</param> + /// <param name="heap">The heap to allocate the block of memory from</param> + /// <returns>A handle to the block of memory</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryHandle<T> SafeAllocNearestPage<T>(IUnmangedHeap heap, int elements, bool zero = false) where T : unmanaged + { + ArgumentOutOfRangeException.ThrowIfNegative(elements); + + return SafeAllocNearestPage<T>(heap, (nuint)elements, zero); } /// <summary> + /// Allocates a block of unmanaged, or pooled manaaged memory depending on + /// compilation flags and runtime unamanged allocators, rounded up to the + /// neareset memory page. + /// </summary> + /// <typeparam name="T">The unamanged type to allocate</typeparam> + /// <param name="elements">The number of elements of the type within the block</param> + /// <param name="zero">Flag to zero elements during allocation before the method returns</param> + /// <param name="heap">The heap to allocate the block of memory from</param> + /// <returns>A handle to the block of memory</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryHandle<T> SafeAllocNearestPage<T>(IUnmangedHeap heap, nuint elements, bool zero = false) where T : unmanaged + => SafeAlloc<T>(heap, elements: NearestPage<T>(elements), zero); + + /// <summary> /// Allocates a structure of the specified type on the specified /// unmanged heap and optionally zero's it's memory /// </summary> @@ -191,13 +359,8 @@ namespace VNLib.Utils.Memory /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="ObjectDisposedException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ref T StructAllocRef<T>(IUnmangedHeap heap, bool zero) where T : unmanaged - { - //Alloc structure - T* ptr = StructAlloc<T>(heap, zero); - //Get a reference and assign it - return ref Unsafe.AsRef<T>(ptr); - } + public static ref T StructAllocRef<T>(IUnmangedHeap heap, bool zero) where T : unmanaged + => ref Unsafe.AsRef<T>(StructAlloc<T>(heap, zero)); /// <summary> /// Frees a structure allocated with <see cref="StructAlloc{T}(IUnmangedHeap, bool)"/> @@ -207,7 +370,8 @@ namespace VNLib.Utils.Memory /// <param name="structPtr">A pointer to the unmanaged structure to free</param> /// <exception cref="ArgumentNullException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void StructFree<T>(IUnmangedHeap heap, T* structPtr) where T : unmanaged => StructFree(heap, (void*)structPtr); + public static void StructFree<T>(IUnmangedHeap heap, T* structPtr) where T : unmanaged + => StructFree(heap, (void*)structPtr); /// <summary> /// Frees a structure allocated with <see cref="StructAllocRef{T}(IUnmangedHeap, bool)"/> @@ -218,7 +382,8 @@ namespace VNLib.Utils.Memory /// <param name="structRef">A reference to the unmanaged structure to free</param> /// <exception cref="ArgumentNullException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void StructFreeRef<T>(IUnmangedHeap heap, ref T structRef) where T : unmanaged => StructFree(heap, Unsafe.AsPointer(ref structRef)); + public static void StructFreeRef<T>(IUnmangedHeap heap, ref T structRef) where T : unmanaged + => StructFree(heap, Unsafe.AsPointer(ref structRef)); /// <summary> /// Frees a structure allocated with <see cref="StructAlloc{T}(IUnmangedHeap, bool)"/> @@ -233,7 +398,7 @@ namespace VNLib.Utils.Memory //Get intpointer IntPtr ptr = (IntPtr)structPtr; - //Free + bool isFree = heap.Free(ref ptr); Debug.Assert(isFree, $"Structure free failed for heap {heap.GetHashCode()}, struct address {ptr:x}"); } @@ -249,39 +414,20 @@ namespace VNLib.Utils.Memory /// <param name="elements">The number of elements of the type within the block</param> /// <param name="zero">Flag to zero elements during allocation before the method returns</param> /// <returns>A handle to the block of memory</returns> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="OutOfMemoryException"></exception> public static UnsafeMemoryHandle<byte> UnsafeAlloc(int elements, bool zero = false) { - if (elements < 0) - { - throw new ArgumentException("Number of elements must be a positive integer", nameof(elements)); - } + ArgumentOutOfRangeException.ThrowIfNegative(elements); if(elements == 0) { return default; } - /* - * We may allocate from the share heap only if the heap is not using locks - * or if the element size could cause performance issues because its too large - * to use a managed array. - * - * We want to avoid allocations, that may end up in the LOH if we can - */ - - if ((Shared.CreationFlags & HeapCreation.UseSynchronization) == 0 || elements > MAX_UNSAFE_POOL_SIZE) - { - // Alloc from heap - IntPtr block = Shared.Alloc((uint)elements, 1, zero); - //Init new handle - return new(Shared, block, elements); - } - else - { - return ArrayPool<byte>.Shared.UnsafeAlloc(elements, zero); - } + return CanUseUnmanagedHeap<byte>(Shared, (uint)elements) + ? UnsafeAlloc<byte>(Shared, elements, zero) + : UnsafeAlloc(ArrayPool<byte>.Shared, elements, zero); } /// <summary> @@ -292,19 +438,15 @@ namespace VNLib.Utils.Memory /// <param name="elements">The number of elements of the type within the block</param> /// <param name="zero">Flag to zero elements during allocation before the method returns</param> /// <returns>A handle to the block of memory</returns> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="OutOfMemoryException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] public static UnsafeMemoryHandle<byte> UnsafeAllocNearestPage(int elements, bool zero = false) { - if (elements < 0) - { - throw new ArgumentException("Number of elements must be a positive integer", nameof(elements)); - } + ArgumentOutOfRangeException.ThrowIfNegative(elements); //Round to nearest page (in bytes) - nint np = NearestPage(elements); - return UnsafeAlloc((int)np, zero); + return UnsafeAlloc(elements: (int)NearestPage(elements), zero); } /// <summary> @@ -314,31 +456,15 @@ namespace VNLib.Utils.Memory /// <param name="elements">The number of elements of the type within the block</param> /// <param name="zero">Flag to zero elements during allocation before the method returns</param> /// <returns>A handle to the block of memory</returns> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="OutOfMemoryException"></exception> public static IMemoryHandle<byte> SafeAlloc(int elements, bool zero = false) { - if (elements < 0) - { - throw new ArgumentException("Number of elements must be a positive integer", nameof(elements)); - } - - /* - * We may allocate from the share heap only if the heap is not using locks - * or if the element size could cause performance issues because its too large - * to use a managed array. - * - * We want to avoid allocations, that may end up in the LOH if we can - */ + ArgumentOutOfRangeException.ThrowIfNegative(elements); - if ((Shared.CreationFlags & HeapCreation.UseSynchronization) == 0 || elements > MAX_UNSAFE_POOL_SIZE) - { - return Shared.Alloc<byte>(elements, zero); - } - else - { - return new ArrayPoolBuffer<byte>(ArrayPool<byte>.Shared, elements, zero); - } + return CanUseUnmanagedHeap<byte>(Shared, (uint)elements) + ? SafeAlloc<byte>(Shared, (nuint)elements, zero) + : SafeAlloc(ArrayPool<byte>.Shared, elements, zero); } /// <summary> @@ -349,18 +475,12 @@ namespace VNLib.Utils.Memory /// <param name="elements">The number of elements of the type within the block</param> /// <param name="zero">Flag to zero elements during allocation before the method returns</param> /// <returns>A handle to the block of memory</returns> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="OutOfMemoryException"></exception> public static IMemoryHandle<byte> SafeAllocNearestPage(int elements, bool zero = false) { - if (elements < 0) - { - throw new ArgumentException("Number of elements must be a positive integer", nameof(elements)); - } - - //Round to nearest page (in bytes) - nint np = NearestPage(elements); - return SafeAlloc((int)np, zero); + ArgumentOutOfRangeException.ThrowIfNegative(elements); + return SafeAlloc(elements: (int)NearestPage(elements), zero); } #endregion diff --git a/lib/Utils/src/Memory/UnmanagedHeapBase.cs b/lib/Utils/src/Memory/UnmanagedHeapBase.cs index 7f42761..a9730b7 100644 --- a/lib/Utils/src/Memory/UnmanagedHeapBase.cs +++ b/lib/Utils/src/Memory/UnmanagedHeapBase.cs @@ -23,6 +23,7 @@ */ using System; +using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; @@ -111,25 +112,32 @@ namespace VNLib.Utils.Memory bool result; //If disposed, set the block handle to zero and exit to avoid raising exceptions during finalization - if (IsClosed || IsInvalid) + if (IsClosed) { block = LPVOID.Zero; return true; } + /* + * Checking for invalid is not really necesasry because + * the only way the handle can be invalidated is + * if some derrived class mutates the handle value + * and doesn't close the handle + */ + Debug.Assert(IsInvalid == false); + if ((flags & HeapCreation.UseSynchronization) > 0) { //wait for lock lock (HeapLock) - { - //Free block + { result = FreeBlock(block); //Release lock before releasing handle } } else { - //No lock + //No lock needed result = FreeBlock(block); } diff --git a/lib/Utils/src/Memory/UnsafeMemoryHandle.cs b/lib/Utils/src/Memory/UnsafeMemoryHandle.cs index d93739d..4041d0b 100644 --- a/lib/Utils/src/Memory/UnsafeMemoryHandle.cs +++ b/lib/Utils/src/Memory/UnsafeMemoryHandle.cs @@ -29,8 +29,6 @@ using System.Runtime.InteropServices; using System.Runtime.CompilerServices; using System.Diagnostics.CodeAnalysis; -using VNLib.Utils.Extensions; - namespace VNLib.Utils.Memory { @@ -62,16 +60,7 @@ namespace VNLib.Utils.Memory public readonly Span<T> Span { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return _handleType switch - { - HandleType.None => Span<T>.Empty, - HandleType.Pool => _poolArr!.AsSpan(0, _length), - HandleType.PrivateHeap => MemoryUtil.GetSpan<T>(_memoryPtr, _length), - _ => throw new InvalidOperationException("Invalid handle type"), - }; - } + get => AsSpan(); } /// <summary> @@ -153,7 +142,7 @@ namespace VNLib.Utils.Memory IntPtr unalloc = _memoryPtr; //Free the unmanaged handle bool unsafeFreed = _heap!.Free(ref unalloc); - Debug.Assert(unsafeFreed, "A previously allocated unsafe memhandle failed to free"); + Debug.Assert(unsafeFreed, "A previously allocated unsafe memhandle failed to free block"); } break; } @@ -193,6 +182,8 @@ namespace VNLib.Utils.Memory { switch (_handleType) { + case HandleType.None: + return ref Unsafe.NullRef<T>(); case HandleType.Pool: return ref MemoryMarshal.GetArrayDataReference(_poolArr!); case HandleType.PrivateHeap: @@ -207,7 +198,7 @@ namespace VNLib.Utils.Memory /// </summary> /// <returns>The memory block that is held by the internl handle</returns> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Span<T> AsSpan() => Span; + public readonly Span<T> AsSpan() => AsSpan(0, _length); /// <summary> /// Returns a <see cref="Span{T}"/> that represents the memory block pointed to by this handle @@ -216,7 +207,7 @@ namespace VNLib.Utils.Memory /// <returns>The desired memory block at the desired element offset</returns> /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Span<T> AsSpan(int start) => Span[start..]; + public readonly Span<T> AsSpan(int start) => AsSpan(start, _length - start); /// <summary> /// Returns a <see cref="Span{T}"/> that represents the memory block pointed to by this handle @@ -226,7 +217,23 @@ namespace VNLib.Utils.Memory /// <returns>The desired memory block at the desired element offset and length</returns> /// <exception cref="ArgumentOutOfRangeException"></exception>" [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Span<T> AsSpan(int start, int length) => Span.Slice(start, length); + public readonly Span<T> AsSpan(int start, int length) + { + ArgumentOutOfRangeException.ThrowIfNegative(start); + ArgumentOutOfRangeException.ThrowIfNegative(length); + ArgumentOutOfRangeException.ThrowIfGreaterThan(length - start, _length); + + /* + * If the handle is empty, a null ref should be returned. The + * check above will gaurd against calling this function on non-empty + * handles. So adding 0 to 0 on the reference should not cause any issues. + */ + + return MemoryMarshal.CreateSpan( + ref Unsafe.Add(ref GetReference(), start), + length + ); + } ///<inheritdoc/> public readonly override int GetHashCode() @@ -248,7 +255,9 @@ namespace VNLib.Utils.Memory /// <returns>True if the other handle points to the same block of memory as the current handle</returns> public readonly bool Equals(in UnsafeMemoryHandle<T> other) { - return _handleType == other._handleType && Length == other.Length && GetHashCode() == other.GetHashCode(); + return _handleType == other._handleType + && Length == other.Length + && GetHashCode() == other.GetHashCode(); } /// <summary> @@ -277,7 +286,7 @@ namespace VNLib.Utils.Memory /// <param name="left"></param> /// <param name="right"></param> /// <returns>True if handles are equal, flase otherwise</returns> - public static bool operator ==(in UnsafeMemoryHandle<T> left, in UnsafeMemoryHandle<T> right) => left.Equals(right); + public static bool operator ==(in UnsafeMemoryHandle<T> left, in UnsafeMemoryHandle<T> right) => left.Equals(in right); /// <summary> /// Equality overload @@ -285,7 +294,7 @@ namespace VNLib.Utils.Memory /// <param name="left"></param> /// <param name="right"></param> /// <returns>True if handles are equal, flase otherwise</returns> - public static bool operator !=(in UnsafeMemoryHandle<T> left, in UnsafeMemoryHandle<T> right) => !left.Equals(right); + public static bool operator !=(in UnsafeMemoryHandle<T> left, in UnsafeMemoryHandle<T> right) => !left.Equals(in right); } -}
\ No newline at end of file +} diff --git a/lib/Utils/src/VnEncoding.cs b/lib/Utils/src/VnEncoding.cs index b7b3783..dd67f9f 100644 --- a/lib/Utils/src/VnEncoding.cs +++ b/lib/Utils/src/VnEncoding.cs @@ -91,64 +91,17 @@ namespace VNLib.Utils /// <returns>The object decoded from the stream</returns> /// <exception cref="JsonException"></exception> /// <exception cref="NotSupportedException"></exception> - public static ValueTask<T?> JSONDeserializeFromBinaryAsync<T>(Stream? data, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + public static ValueTask<T?> JSONDeserializeFromBinaryAsync<T>( + Stream? data, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default + ) { //Return default if null - return data == null || data.Length == 0 ? ValueTask.FromResult<T?>(default) : JsonSerializer.DeserializeAsync<T>(data, options, cancellationToken); - } - - /// <summary> - /// Attempts to deserialze a json object from a stream of UTF8 data - /// </summary> - /// <param name="data">Binary data to read from</param> - /// <param name="type"></param> - /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param> - /// <param name="cancellationToken"></param> - /// <returns>The object decoded from the stream</returns> - /// <exception cref="JsonException"></exception> - /// <exception cref="NotSupportedException"></exception> - public static ValueTask<object?> JSONDeserializeFromBinaryAsync(Stream? data, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) - { - //Return default if null - return data == null || data.Length == 0 ? ValueTask.FromResult<object?>(default) : JsonSerializer.DeserializeAsync(data, type, options, cancellationToken); - } - - /// <summary> - /// Attempts to serialize the object to json and write the encoded data to the stream - /// </summary> - /// <typeparam name="T">The object type to serialize</typeparam> - /// <param name="data">The object to serialize</param> - /// <param name="output">The <see cref="Stream"/> to write output data to</param> - /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to serializer</param> - /// <exception cref="JsonException"></exception> - public static void JSONSerializeToBinary<T>(T data, Stream output, JsonSerializerOptions? options = null) - { - //return if null - if(data == null) - { - return; - } - //Serialize - JsonSerializer.Serialize(output, data, options); - } - /// <summary> - /// Attempts to serialize the object to json and write the encoded data to the stream - /// </summary> - /// <param name="data">The object to serialize</param> - /// <param name="output">The <see cref="Stream"/> to write output data to</param> - /// <param name="type"></param> - /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to serializer</param> - /// <exception cref="JsonException"></exception> - public static void JSONSerializeToBinary(object data, Stream output, Type type, JsonSerializerOptions? options = null) - { - //return if null - if (data == null) - { - return; - } - //Serialize - JsonSerializer.Serialize(output, data, type, options); - } + return data == null || data.Length == 0 + ? ValueTask.FromResult<T?>(default) + : JsonSerializer.DeserializeAsync<T>(data, options, cancellationToken); + } #region Base32 @@ -250,11 +203,8 @@ namespace VNLib.Utils //right shift the value to lower 5 bits val >>= 3; - //Lookup charcode - char base32Char = RFC_4648_BASE32_CHARS[val]; - //append the character to the writer - writer.Append(base32Char); + writer.Append(RFC_4648_BASE32_CHARS[val]); //Shift input left by 5 bits so the next 5 bits can be read inputAsLong <<= 5; @@ -282,6 +232,15 @@ namespace VNLib.Utils } /// <summary> + /// Gets the size of the buffer required to decode a base32 encoded + /// string. This buffer size will always be smaller than the input size. + /// </summary> + /// <param name="inputSize">The base32 encoded data input size</param> + /// <returns>The size of the output buffer needed to write decoded data to</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint Base32DecodedSizeSize(nint inputSize) => (inputSize * 5) / 8; + + /// <summary> /// Attempts to decode the Base32 encoded string /// </summary> /// <param name="input">The Base32 encoded data to decode</param> @@ -292,53 +251,57 @@ namespace VNLib.Utils { //TODO support Big-Endian byte order + int count = 0; + ulong bufferLong = 0; //buffer used to shift data while decoding + byte* buffer = (byte*)&bufferLong; //re-cast to byte* to use it as a byte buffer + //trim padding characters input = input.Trim('='); //Calc the number of bytes to write - int outputSize = (input.Length * 5) / 8; + nint outputSize = Base32DecodedSizeSize(input.Length); //make sure the output buffer is large enough if(writer.RemainingSize < outputSize) { return false; } - - //buffer used to shift data while decoding - ulong bufferLong = 0; - - //re-cast to byte* to index it as a byte buffer - byte* buffer = (byte*)&bufferLong; - - int count = 0, len = input.Length; - while(count < len) + while(count < input.Length) { - //Convert the character to its char code - byte charCode = GetCharCode(input[count]); - - //write byte to buffer - buffer[0] |= charCode; + /* + * Attempts to accumulate 8 bytes from the input buffer + * and write it from hi-lo byte order to the output buffer + * + * The underlying 64-bit integer is shifted left by 5 bits + * on every loop, removing leading zero bits. The OR operation + * ignores the zeros when the next byte is written, and anything + * leading is shifted off the end when 8 bytes are written. + * + * Reemeber: each character only contains 5 bits of useful data + */ + + buffer[0] |= GetCharCode(input[count]); count++; //If 8 characters have been decoded, reset the buffer - if((count % 8) == 0) + if ((count % 8) == 0) { //Write the 5 upper bytes in reverse order to the output buffer for(int j = 0; j < 5; j++) { writer.Append(buffer[4 - j]); } - //reset + bufferLong = 0; } - //left shift the buffer up by 5 bits + //left shift the buffer up by 5 bits, because thats all we bufferLong <<= 5; } - //If remaining data has not be written, but has been buffed, finalize it + //If remaining data has not be written, but has been bufferedd, finalize it if (writer.Written < outputSize) { //calculate how many bits the buffer still needs to be shifted by (will be 5 bits off because of the previous loop) @@ -348,7 +311,7 @@ namespace VNLib.Utils bufferLong <<= remainingShift; //calc remaining bytes - int remaining = (outputSize - writer.Written); + nint remaining = (outputSize - writer.Written); //Write remaining bytes to the output for(int i = 0; i < remaining; i++) @@ -455,31 +418,24 @@ namespace VNLib.Utils /// <returns>The base32 encoded string representation of the specified buffer</returns> /// <exception cref="InternalBufferTooSmallException"></exception> public static string ToBase32String(ReadOnlySpan<byte> binBuffer, bool withPadding = false) - { - string value; + { //Calculate the base32 entropy to alloc an appropriate buffer (minium buffer of 2 chars) int entropy = Base32CalcMaxBufferSize(binBuffer.Length); - - //Alloc buffer for enough size (2*long bytes) is not an issue + using UnsafeMemoryHandle<char> charBuffer = MemoryUtil.UnsafeAlloc<char>(entropy); //Encode ERRNO encoded = TryToBase32Chars(binBuffer, charBuffer.Span); + if (!encoded) { throw new InternalBufferTooSmallException("Base32 char buffer was too small"); } - //Convert with or w/o padding - if (withPadding) - { - value = charBuffer.Span[0..(int)encoded].ToString(); - } - else - { - value = charBuffer.Span[0..(int)encoded].Trim('=').ToString(); - } - return value; + //Convert with or w/o padding + return withPadding + ? charBuffer.Span[0..(int)encoded].ToString() + : charBuffer.Span[0..(int)encoded].Trim('=').ToString(); } /// <summary> @@ -494,12 +450,14 @@ namespace VNLib.Utils { //calc size of bin buffer int size = base32.Length; - //Rent a bin buffer + using UnsafeMemoryHandle<byte> binBuffer = MemoryUtil.UnsafeAlloc(size); - //Try to decode the data + ERRNO decoded = TryFromBase32Chars(base32, binBuffer.Span); - //Marshal back to a struct - return decoded ? MemoryMarshal.Read<T>(binBuffer.Span[..(int)decoded]) : throw new InternalBufferTooSmallException("Binbuffer was too small"); + + return decoded + ? MemoryMarshal.Read<T>(binBuffer.Span[..(int)decoded]) + : throw new InternalBufferTooSmallException("Binbuffer was too small"); } /// <summary> @@ -536,11 +494,11 @@ namespace VNLib.Utils { //get the size of the structure int binSize = Unsafe.SizeOf<T>(); - //Rent a bin buffer + Span<byte> binBuffer = stackalloc byte[binSize]; - //Write memory to buffer + MemoryMarshal.Write(binBuffer, in value); - //Convert to base32 + return ToBase32String(binBuffer, withPadding); } @@ -548,28 +506,9 @@ namespace VNLib.Utils #region percent encoding - private const int MAX_STACKALLOC = 1024; + private const int MAX_STACKALLOC = 512; - private static readonly ReadOnlyMemory<byte> HexToUtf8Pos = new byte[16] - { - 0x30, //0 - 0x31, //1 - 0x32, //2 - 0x33, //3 - 0x34, //4 - 0x35, //5 - 0x36, //6 - 0x37, //7 - 0x38, //8 - 0x39, //9 - - 0x41, //A - 0x42, //B - 0x43, //C - 0x44, //D - 0x45, //E - 0x46 //F - }; + private static readonly byte[] HexToUtf8Pos = "0123456789ABCDEF"u8.ToArray(); /// <summary> /// Deterimes the size of the buffer needed to encode a utf8 encoded @@ -630,7 +569,7 @@ namespace VNLib.Utils public static ERRNO PercentEncode(ReadOnlySpan<byte> utf8Bytes, Span<byte> utf8Output, ReadOnlySpan<byte> allowedChars = default) { int outPos = 0, len = utf8Bytes.Length; - ReadOnlySpan<byte> lookupTable = HexToUtf8Pos.Span; + ReadOnlySpan<byte> lookupTable = HexToUtf8Pos.AsSpan(); if (allowedChars.IsEmpty) { @@ -645,11 +584,13 @@ namespace VNLib.Utils } else { - //Percent encode + /* + * Leading byte is %, followed by a single byte + * for the hi and low nibble of the value + */ + utf8Output[outPos++] = 0x25; // '%' - //Calc and store the encoded by the upper 4 bits utf8Output[outPos++] = lookupTable[(value & 0xf0) >> 4]; - //Store lower 4 bits in encoded value utf8Output[outPos++] = lookupTable[value & 0x0f]; } } @@ -667,11 +608,13 @@ namespace VNLib.Utils } else { - //Percent encode - utf8Output[outPos++] = 0x25; // '%' - //Calc and store the encoded by the upper 4 bits + /* + * Leading byte is %, followed by a single byte + * for the hi and low nibble of the value + */ + + utf8Output[outPos++] = 0x25; // '%' utf8Output[outPos++] = lookupTable[(value & 0xf0) >> 4]; - //Store lower 4 bits in encoded value utf8Output[outPos++] = lookupTable[value & 0x0f]; } } @@ -711,7 +654,7 @@ namespace VNLib.Utils public static ERRNO PercentDecode(ReadOnlySpan<byte> utf8Encoded, Span<byte> utf8Output) { int outPos = 0, len = utf8Encoded.Length; - ReadOnlySpan<byte> lookupTable = HexToUtf8Pos.Span; + ReadOnlySpan<byte> lookupTable = HexToUtf8Pos.AsSpan(); for (int i = 0; i < len; i++) { @@ -816,9 +759,13 @@ namespace VNLib.Utils /// return value. The default value is System.Base64FormattingOptions.None. /// </param> /// <returns>The number of characters encoded, or <see cref="ERRNO.E_FAIL"/> if conversion was unsuccessful</returns> - public static ERRNO TryToBase64Chars(ReadOnlySpan<byte> buffer, Span<char> base64, Base64FormattingOptions options = Base64FormattingOptions.None) + public static ERRNO TryToBase64Chars( + ReadOnlySpan<byte> buffer, + Span<char> base64, + Base64FormattingOptions options = Base64FormattingOptions.None + ) { - return Convert.TryToBase64Chars(buffer, base64, out int charsWritten, options: options) ? charsWritten : ERRNO.E_FAIL; + return Convert.TryToBase64Chars(buffer, base64, out int charsWritten, options) ? charsWritten : ERRNO.E_FAIL; } @@ -905,8 +852,17 @@ namespace VNLib.Utils /// <returns>The size of the <paramref name="base64"/> buffer</returns> public static ERRNO Base64ToUrlSafe(ReadOnlySpan<byte> base64, Span<byte> base64Url) { + ArgumentOutOfRangeException.ThrowIfLessThan(base64.Length, base64Url.Length, nameof(base64)); + //Aligned copy to the output buffer - base64.CopyTo(base64Url); + MemoryUtil.Memmove( + ref MemoryMarshal.GetReference(base64Url), + 0, + ref MemoryMarshal.GetReference(base64), + 0, + (nuint)base64Url.Length + ); + //One time convert the output buffer to url safe Base64ToUrlSafeInPlace(base64Url); return base64.Length; @@ -923,8 +879,17 @@ namespace VNLib.Utils /// <returns>The size of the <paramref name="base64Url"/> buffer</returns> public static ERRNO Base64FromUrlSafe(ReadOnlySpan<byte> base64Url, Span<byte> base64) { + ArgumentOutOfRangeException.ThrowIfLessThan(base64.Length, base64Url.Length, nameof(base64)); + //Aligned copy to the output buffer - base64Url.CopyTo(base64); + MemoryUtil.Memmove( + ref MemoryMarshal.GetReference(base64Url), + 0, + ref MemoryMarshal.GetReference(base64), + 0, + (nuint)base64Url.Length + ); + //One time convert the output buffer to url safe Base64FromUrlSafeInPlace(base64); return base64Url.Length; @@ -982,27 +947,62 @@ namespace VNLib.Utils //Set the encoding to utf8 encoding ??= Encoding.UTF8; + //get the number of bytes to alloc a buffer int decodedSize = encoding.GetByteCount(chars); if(decodedSize > MAX_STACKALLOC) { - //Alloc heap buffer using UnsafeMemoryHandle<byte> decodeHandle = MemoryUtil.UnsafeAlloc(decodedSize); + //Get the utf8 binary data int count = encoding.GetBytes(chars, decodeHandle.Span); return Base64UrlDecode(decodeHandle.Span[..count], output); } else - { - //Alloc stack buffer + { Span<byte> decodeBuffer = stackalloc byte[decodedSize]; + //Get the utf8 binary data int count = encoding.GetBytes(chars, decodeBuffer); return Base64UrlDecode(decodeBuffer[..count], output); } } + private static string ConvertToBase64UrlStringInternal( + ReadOnlySpan<byte> rawData, + Span<byte> buffer, + bool includePadding, + Encoding encoding + ) + { + //Conver to base64 + OperationStatus status = Base64.EncodeToUtf8(rawData, buffer, out _, out int written, true); + + //Check for invalid states + Debug.Assert(status != OperationStatus.DestinationTooSmall, "Buffer allocation was too small for the conversion"); + Debug.Assert(status != OperationStatus.NeedMoreData, "Need more data status was returned but is not valid for an encoding operation"); + + //Should never occur, but just in case, this is an input error + if (status == OperationStatus.InvalidData) + { + throw new ArgumentException("Your input data contained values that could not be converted to base64", nameof(rawData)); + } + + Span<byte> base64 = buffer[..written]; + + //Make url safe + Base64ToUrlSafeInPlace(base64); + + //Remove padding + if (!includePadding) + { + base64 = base64.TrimEnd((byte)0x3d); + } + + //Convert to string + return encoding.GetString(base64); + } /// <summary> /// Base64url encodes the binary buffer to its utf8 binary representation @@ -1068,58 +1068,8 @@ namespace VNLib.Utils /// <param name="includePadding">A value that indicates if the base64 padding characters should be included</param> /// <returns>The base64url encoded string</returns> /// <exception cref="ArgumentException"></exception> - public static string ToBase64UrlSafeString(ReadOnlySpan<byte> rawData, bool includePadding) - { - if (rawData.IsEmpty) - { - throw new ArgumentException("The input buffer was empty", nameof(rawData)); - } - - int maxBufSize = Base64.GetMaxEncodedToUtf8Length(rawData.Length); - - if(maxBufSize > MAX_STACKALLOC) - { - //Alloc heap buffer - using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAllocNearestPage(maxBufSize); - return ConvertToBase64UrlStringInternal(rawData, buffer.Span, includePadding); - } - else - { - //Stack alloc buffer - Span<byte> buffer = stackalloc byte[maxBufSize]; - return ConvertToBase64UrlStringInternal(rawData, buffer, includePadding); - } - } - - private static string ConvertToBase64UrlStringInternal(ReadOnlySpan<byte> rawData, Span<byte> buffer, bool includePadding) - { - //Conver to base64 - OperationStatus status = Base64.EncodeToUtf8(rawData, buffer, out _, out int written, true); - - //Check for invalid states - Debug.Assert(status != OperationStatus.DestinationTooSmall, "Buffer allocation was too small for the conversion"); - Debug.Assert(status != OperationStatus.NeedMoreData, "Need more data status was returned but is not valid for an encoding operation"); - - //Should never occur, but just in case, this is an input error - if (status == OperationStatus.InvalidData) - { - throw new ArgumentException("Your input data contained values that could not be converted to base64", nameof(rawData)); - } - - Span<byte> base64 = buffer[..written]; - - //Make url safe - Base64ToUrlSafeInPlace(base64); - - //Remove padding - if (!includePadding) - { - base64 = base64.TrimEnd((byte)0x3d); - } - - //Convert to string - return Encoding.UTF8.GetString(base64); - } + [Obsolete("Use Base64UrlEncode instead")] + public static string ToBase64UrlSafeString(ReadOnlySpan<byte> rawData, bool includePadding) => Base64UrlEncode(rawData, includePadding); /// <summary> /// Encodes the binary input buffer to its base64url safe utf8 encoding, and writes the output @@ -1155,6 +1105,41 @@ namespace VNLib.Utils } /// <summary> + /// Encodes the binary intput buffer to its base64url safe encoding, then converts the binary + /// value to it character encoded value and allocates a new string. Defaults to UTF8 character + /// encoding. Base64url is a subset of ASCII,UTF7,UTF8,UTF16 etc so most encodings should be safe. + /// </summary> + /// <param name="input">The input binary intput buffer</param> + /// <param name="includePadding">A value that indicates if base64 padding should be url encoded(true), or removed(false).</param> + /// <param name="encoding">The encoding used to convert the binary buffer to its character representation.</param> + /// <returns>The base64url encoded string of the input data using the desired encoding</returns> + public static string Base64UrlEncode(ReadOnlySpan<byte> input, bool includePadding, Encoding? encoding = null) + { + if (input.IsEmpty) + { + return string.Empty; + } + + encoding ??= Encoding.UTF8; + + //We need to alloc an intermediate buffer, get the base64 max size + int maxSize = Base64.GetMaxEncodedToUtf8Length(input.Length); + + if (maxSize > MAX_STACKALLOC) + { + //Alloc heap buffer + using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAlloc(maxSize); + return ConvertToBase64UrlStringInternal(input, buffer.Span, includePadding, encoding); + } + else + { + //Alloc stack buffer + Span<byte> bufer = stackalloc byte[maxSize]; + return ConvertToBase64UrlStringInternal(input, bufer, includePadding, encoding); + } + } + + /// <summary> /// Encodes the binary intput buffer to its base64url safe encoding, then converts the internal buffer /// to its character encoding using the supplied <paramref name="encoding"/>, and writes the characters /// to the output buffer. Defaults to UTF8 character encoding. Base64url is a subset of ASCII,UTF7,UTF8,UTF16 etc diff --git a/lib/Utils/tests/IO/VnMemoryStreamTests.cs b/lib/Utils/tests/IO/VnMemoryStreamTests.cs index 9742197..9bcb823 100644 --- a/lib/Utils/tests/IO/VnMemoryStreamTests.cs +++ b/lib/Utils/tests/IO/VnMemoryStreamTests.cs @@ -95,5 +95,49 @@ namespace VNLib.Utils.IO.Tests Assert.ThrowsException<NotSupportedException>(() => vms.WriteByte(0)); } + + [TestMethod()] + public void GetMemOrSpanTest() + { + //Alloc stream with some initial buffer size + using VnMemoryStream vms = new(1024, false); + + //Ensure since no data was written, the returned windows are empty + Assert.IsTrue(vms.AsSpan().IsEmpty); + Assert.IsTrue(vms.AsMemory().IsEmpty); + + //Write some data + byte[] testData = [1, 2, 3, 4, 5, 6, 7, 8]; + vms.Write(testData); + + Assert.AreEqual(vms.Length, testData.Length); + + //Get the data as a span + ReadOnlySpan<byte> span = vms.AsSpan(); + Assert.AreEqual(span.Length, testData.Length); + + for (int i = 0; i < span.Length; i++) + { + Assert.AreEqual(span[i], testData[i]); + } + + //Get the data as a memory + ReadOnlyMemory<byte> memory = vms.AsMemory(); + Assert.AreEqual(memory.Length, testData.Length); + + for (int i = 0; i < memory.Length; i++) + { + Assert.AreEqual(memory.Span[i], testData[i]); + } + + //Get the data as a byte array + byte[] array = vms.ToArray(); + Assert.AreEqual(array.Length, testData.Length); + + for (int i = 0; i < array.Length; i++) + { + Assert.AreEqual(array[i], testData[i]); + } + } } }
\ No newline at end of file diff --git a/lib/Utils/tests/Memory/MemoryUtilTests.cs b/lib/Utils/tests/Memory/MemoryUtilTests.cs index 63342b5..68bc35c 100644 --- a/lib/Utils/tests/Memory/MemoryUtilTests.cs +++ b/lib/Utils/tests/Memory/MemoryUtilTests.cs @@ -202,7 +202,7 @@ namespace VNLib.Utils.Memory.Tests { } //test against negative number - Assert.ThrowsException<ArgumentException>(() => MemoryUtil.UnsafeAlloc<byte>(-1)); + Assert.ThrowsException<ArgumentOutOfRangeException>(() => MemoryUtil.UnsafeAlloc<byte>(-1)); //Alloc large block test (100mb) const int largTestSize = 100000 * 1024; @@ -257,7 +257,7 @@ namespace VNLib.Utils.Memory.Tests } //Negative value - Assert.ThrowsException<ArgumentException>(() => _ = MemoryUtil.UnsafeAlloc<byte>(-1)); + Assert.ThrowsException<ArgumentOutOfRangeException>(() => _ = MemoryUtil.UnsafeAlloc<byte>(-1)); /* @@ -315,7 +315,7 @@ namespace VNLib.Utils.Memory.Tests } //Negative value - Assert.ThrowsException<ArgumentException>(() => _ = MemoryUtil.SafeAlloc<byte>(-1)); + Assert.ThrowsException<ArgumentOutOfRangeException>(() => _ = MemoryUtil.SafeAlloc<byte>(-1)); /* * Alloc random sized blocks in a loop, confirm they are empty diff --git a/lib/Utils/tests/VNLib.UtilsTests.csproj b/lib/Utils/tests/VNLib.UtilsTests.csproj index 83013ff..0a425a9 100644 --- a/lib/Utils/tests/VNLib.UtilsTests.csproj +++ b/lib/Utils/tests/VNLib.UtilsTests.csproj @@ -16,9 +16,9 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" /> - <PackageReference Include="MSTest.TestAdapter" Version="3.3.1" /> - <PackageReference Include="MSTest.TestFramework" Version="3.3.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" /> + <PackageReference Include="MSTest.TestAdapter" Version="3.5.0" /> + <PackageReference Include="MSTest.TestFramework" Version="3.5.0" /> <PackageReference Include="coverlet.collector" Version="6.0.2"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |