From 55859158fbd0bf54473a0baeb486045a025c7c5d Mon Sep 17 00:00:00 2001 From: vnugent Date: Sun, 24 Mar 2024 21:01:06 -0400 Subject: Squashed commit of the following: commit 6c1667be23597513537f8190e2f55d65eb9b7c7a Author: vnugent Date: Fri Mar 22 12:01:53 2024 -0400 refactor: Overhauled native library loading and lazy init commit ebf688f2f974295beabf7b5def7e6f6f150551d0 Author: vnugent Date: Wed Mar 20 22:16:17 2024 -0400 refactor: Update compression header files and macros + Ci build commit 9c7b564911080ccd5cbbb9851a0757b05e1e9047 Author: vnugent Date: Tue Mar 19 21:54:49 2024 -0400 refactor: JWK overhaul & add length getter to FileUpload commit 6d8c3444e09561e5957491b3cc1ae858e0abdd14 Author: vnugent Date: Mon Mar 18 16:13:20 2024 -0400 feat: Add FNV1a software checksum and basic correction tests commit 00d182088cecefc08ca80b1faee9bed3f215f40b Author: vnugent Date: Fri Mar 15 01:05:27 2024 -0400 chore: #6 Use utils filewatcher instead of built-in commit d513c10d9895c6693519ef1d459c6a5a76929436 Author: vnugent Date: Sun Mar 10 21:58:14 2024 -0400 source tree project location updated --- lib/Utils/src/ArgumentList.cs | 9 +- lib/Utils/src/Memory/MemoryUtil.cs | 37 ++-- lib/Utils/src/Native/NatveLibraryResolver.cs | 299 +++++++++++++++++++++++++++ lib/Utils/src/Native/SafeLibraryHandle.cs | 260 ++++++++++------------- lib/Utils/src/Resources/LazyInitializer.cs | 112 ++++++++++ 5 files changed, 544 insertions(+), 173 deletions(-) create mode 100644 lib/Utils/src/Native/NatveLibraryResolver.cs create mode 100644 lib/Utils/src/Resources/LazyInitializer.cs (limited to 'lib/Utils') diff --git a/lib/Utils/src/ArgumentList.cs b/lib/Utils/src/ArgumentList.cs index 235e62c..c02ebee 100644 --- a/lib/Utils/src/ArgumentList.cs +++ b/lib/Utils/src/ArgumentList.cs @@ -24,6 +24,7 @@ using System; using System.Linq; +using System.Collections; using System.Collections.Generic; namespace VNLib.Utils @@ -31,7 +32,7 @@ namespace VNLib.Utils /// /// Provides methods for parsing an argument list /// - public class ArgumentList : IIndexable + public class ArgumentList : IIndexable, IEnumerable { private readonly List _args; @@ -96,6 +97,10 @@ namespace VNLib.Utils return index == -1 || index + 1 >= _args.Count ? null : this[index + 1]; } - + /// + public IEnumerator GetEnumerator() => _args.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } \ No newline at end of file diff --git a/lib/Utils/src/Memory/MemoryUtil.cs b/lib/Utils/src/Memory/MemoryUtil.cs index 6efd5ba..b6eadbc 100644 --- a/lib/Utils/src/Memory/MemoryUtil.cs +++ b/lib/Utils/src/Memory/MemoryUtil.cs @@ -25,7 +25,6 @@ using System; using System.Buffers; using System.Security; -using System.Threading; using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; @@ -33,6 +32,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics.X86; using VNLib.Utils.Memory.Diagnostics; +using VNLib.Utils.Resources; namespace VNLib.Utils.Memory { @@ -102,13 +102,13 @@ namespace VNLib.Utils.Memory /// The backing heap /// is determined by the OS type and process environment varibles. /// - public static IUnmangedHeap Shared => _sharedHeap.Value; + public static IUnmangedHeap Shared => _lazyHeap.Instance; - - private static readonly Lazy _sharedHeap = InitHeapInternal(); + + private static readonly LazyInitializer _lazyHeap = InitHeapInternal(); //Avoiding static initializer - private static Lazy InitHeapInternal() + private static LazyInitializer InitHeapInternal() { //Get env for heap diag _ = ERRNO.TryParse(Environment.GetEnvironmentVariable(SHARED_HEAP_ENABLE_DIAGNOISTICS_ENV), out ERRNO diagEnable); @@ -116,22 +116,17 @@ namespace VNLib.Utils.Memory Trace.WriteLineIf(diagEnable, "Shared heap diagnostics enabled"); Trace.WriteLineIf(globalZero, "Shared heap global zero enabled"); - - Lazy heap = new (() => InitHeapInternal(true, diagEnable, globalZero), LazyThreadSafetyMode.PublicationOnly); - - //Cleanup the heap on process exit - AppDomain.CurrentDomain.DomainUnload += DomainUnloaded; - - return heap; - } - private static void DomainUnloaded(object? sender, EventArgs e) - { - //Dispose the heap if allocated - if (_sharedHeap.IsValueCreated) + return new(() => { - _sharedHeap.Value.Dispose(); - } + //Init shared heap instance + IUnmangedHeap heap = InitHeapInternal(true, diagEnable, globalZero); + + //Register domain unload event + AppDomain.CurrentDomain.DomainUnload += (_, _) => heap.Dispose(); + + return heap; + }); } /// @@ -147,7 +142,7 @@ namespace VNLib.Utils.Memory * If heap is allocated and the heap type is a tracked heap, * get the heap's stats, otherwise return an empty handle */ - return _sharedHeap.IsValueCreated && _sharedHeap.Value is TrackedHeapWrapper h + return _lazyHeap.IsLoaded && _lazyHeap.Instance is TrackedHeapWrapper h ? h.GetCurrentStats() : default; } @@ -1610,4 +1605,4 @@ namespace VNLib.Utils.Memory public readonly void Validate(nuint count) => CheckBounds(handle, offset, count); } } -} \ No newline at end of file +} diff --git a/lib/Utils/src/Native/NatveLibraryResolver.cs b/lib/Utils/src/Native/NatveLibraryResolver.cs new file mode 100644 index 0000000..b888f42 --- /dev/null +++ b/lib/Utils/src/Native/NatveLibraryResolver.cs @@ -0,0 +1,299 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: NatveLibraryResolver.cs +* +* NatveLibraryResolver.cs is part of VNLib.Utils which is part of +* the larger VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Diagnostics; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Diagnostics.CodeAnalysis; + +namespace VNLib.Utils.Native +{ + /// + /// Uses a supplied library state to resolve and load a platform native library + /// into the process memory space. + /// + /// The raw caller-supplied library path to resolve + /// A assembly loading the desired library + /// The dll loader search path requirements + internal readonly struct NatveLibraryResolver(string suppliedLibraryPath, Assembly assembly, DllImportSearchPath searchPath) + { + private readonly string _libFileName = Path.GetFileName(suppliedLibraryPath); + private readonly string? _relativeDir = Path.GetDirectoryName(suppliedLibraryPath); + + internal readonly bool HasRelativeDir => _relativeDir != null; + + internal readonly bool IsFullPath => Path.IsPathRooted(suppliedLibraryPath); + + /// + /// Resolves and attempts to load the current library into the current process. + /// + /// The if the library was successfully resolved + /// True if the library was resolved and loaded into the process, false otherwise + internal readonly bool ResolveAndLoadLibrary([NotNullWhen(true)] out SafeLibraryHandle? library) + { + //Try naive load if the path is an absolute path + if (IsFullPath && _tryLoad(suppliedLibraryPath, out library)) + { + return true; + } + + //Try to load the library from the relative directory + if (HasRelativeDir && TryLoadRelativeToWorkingDir(out library)) + { + return true; + } + + //Path is just a file name, so search for prefixes and extensions loading directly + if (TryNaiveLoadLibrary(out library)) + { + return true; + } + + //Try searching for the file in directories + return TryLoadFromSafeDirs(out library); + } + + /* + * Naive load builds file names that are platform dependent + * and probes for the library using the NativeLibrary.TryLoad rules + * to load the library. + * + * If the file has an extension, it will attempt to load the library + * directly using the file name. Otherwise, it will continue to probe + */ + private readonly bool TryNaiveLoadLibrary([NotNullWhen(true)] out SafeLibraryHandle? library) + { + foreach (string probingFileName in GetProbingFileNames(_libFileName)) + { + if (_tryLoad(probingFileName, out library)) + { + return true; + } + } + + library = null; + return false; + } + + /* + * Attempts to probe library files names that are located inside safe directories + * specified by the caller using the DllImportSearchPath enum. + */ + private readonly bool TryLoadFromSafeDirs([NotNullWhen(true)] out SafeLibraryHandle? library) + { + //Try enumerating safe directories + if (searchPath.HasFlag(DllImportSearchPath.SafeDirectories)) + { + foreach (string dir in GetSpecialDirPaths()) + { + if (TryLoadInDirectory(dir, out library)) + { + return true; + } + } + } + + //Check application directory first (including subdirectories) + if (searchPath.HasFlag(DllImportSearchPath.ApplicationDirectory)) + { + //get the current directory + if (TryLoadInDirectory(Directory.GetCurrentDirectory(), out library)) + { + return true; + } + } + + //See if search in the calling assembly directory + if (searchPath.HasFlag(DllImportSearchPath.AssemblyDirectory)) + { + //Get the calling assmblies directory + string libDir = Assembly.GetCallingAssembly().Location; + Debug.WriteLine("Native library searching for calling assembly location:{0} ", libDir); + if (TryLoadInDirectory(libDir, out library)) + { + return true; + } + } + + //Search system32 dir + if (searchPath.HasFlag(DllImportSearchPath.System32)) + { + string sys32Dir = Environment.GetFolderPath(Environment.SpecialFolder.SystemX86); + + //Get the system directory + if (TryLoadInDirectory(sys32Dir, out library)) + { + return true; + } + } + + library = null; + return false; + } + + /* + * Users may specify realtive directories to search for the library + * in the current working directory, so this function attempts to load + * the library from the relative directory if the user has specified one + */ + private readonly bool TryLoadRelativeToWorkingDir([NotNullWhen(true)] out SafeLibraryHandle? library) + { + string libDir = Directory.GetCurrentDirectory(); + return TryLoadInDirectory(Path.Combine(libDir, _relativeDir!), out library); + } + + /* + * Attempts to load libraries that are located in the specified directory + * by probing for the library file name in the directory path using + * prefixes and extensions + */ + private readonly bool TryLoadInDirectory(string baseDir, [NotNullWhen(true)] out SafeLibraryHandle? library) + { + IEnumerable fullProbingPaths = GetProbingFileNames(_libFileName).Select(p => Path.Combine(baseDir, p)); + + foreach (string probingFilePath in fullProbingPaths) + { + if (_tryLoad(probingFilePath, out library)) + { + return true; + } + } + + library = null; + return false; + } + + /* + * core load function + */ + internal readonly bool _tryLoad(string filePath, [NotNullWhen(true)] out SafeLibraryHandle? library) + { + //Attempt a naive load + if (NativeLibrary.TryLoad(filePath, assembly, searchPath, out IntPtr libHandle)) + { + library = SafeLibraryHandle.FromExisting(libHandle, true); + return true; + } + + library = null; + return false; + } + + private static readonly Environment.SpecialFolder[] SafeDirs = + [ + Environment.SpecialFolder.SystemX86, + Environment.SpecialFolder.System, + Environment.SpecialFolder.Windows, + Environment.SpecialFolder.ProgramFilesX86, + Environment.SpecialFolder.ProgramFiles + ]; + + private static IEnumerable GetSpecialDirPaths() => SafeDirs.Select(Environment.GetFolderPath); + + private static IEnumerable GetProbingFileNames(string libraryFileName) + { + //Inlcude the library file name if it has an extension + if (Path.HasExtension(libraryFileName)) + { + yield return libraryFileName; + } + + foreach (string prefix in GetNativeLibPrefixs()) + { + foreach (string libExtension in GetNativeLibExtension()) + { + yield return GetLibraryFileName(prefix, libraryFileName, libExtension); + } + } + + static string GetLibraryFileName(string prefix, string libPath, string extension) + { + //Get dir name from the lib path if it has one + + + libPath = Path.Combine(prefix, libPath); + + //If the library path already has an extension, just search for the file + if (!Path.HasExtension(libPath)) + { + //slice the lib to its file name + libPath = Path.GetFileName(libPath); + + if (extension.Length > 0) + { + libPath = Path.ChangeExtension(libPath, extension); + } + } + + return libPath; + } + + static string[] GetNativeLibPrefixs() + { + if (OperatingSystem.IsWindows()) + { + return [""]; + } + else if (OperatingSystem.IsMacOS()) + { + return ["", "lib"]; + } + else if (OperatingSystem.IsLinux()) + { + return ["", "lib_", "lib"]; + } + else + { + Debug.Fail("Unknown OS type"); + return []; + } + } + + static string[] GetNativeLibExtension() + { + if (OperatingSystem.IsWindows()) + { + return ["", ".dll"]; + } + else if (OperatingSystem.IsMacOS()) + { + return ["", ".dylib"]; + } + else if (OperatingSystem.IsLinux()) + { + return ["", ".so", ".so.1"]; + } + else + { + Debug.Fail("Unknown OS type"); + return []; + } + } + } + } +} diff --git a/lib/Utils/src/Native/SafeLibraryHandle.cs b/lib/Utils/src/Native/SafeLibraryHandle.cs index ed9dd16..5fb2283 100644 --- a/lib/Utils/src/Native/SafeLibraryHandle.cs +++ b/lib/Utils/src/Native/SafeLibraryHandle.cs @@ -23,10 +23,7 @@ */ using System; -using System.IO; -using System.Linq; using System.Reflection; -using System.Diagnostics; using System.Runtime.InteropServices; using System.Diagnostics.CodeAnalysis; @@ -42,128 +39,7 @@ namespace VNLib.Utils.Native /// public override bool IsInvalid => handle == IntPtr.Zero; - private SafeLibraryHandle(IntPtr libHandle) : base(IntPtr.Zero, true) - { - //Init handle - SetHandle(libHandle); - } - - /// - /// Finds and loads the specified native libary into the current process by its name at runtime - /// - /// The path (or name of libary) to search for - /// - /// The used to search for libaries - /// within the current filesystem - /// - /// The loaded - /// - /// - public static SafeLibraryHandle LoadLibrary(string libPath, DllImportSearchPath searchPath = DllImportSearchPath.ApplicationDirectory) - { - ArgumentNullException.ThrowIfNull(libPath); - //See if the path includes a file extension - return TryLoadLibrary(libPath, searchPath, out SafeLibraryHandle? lib) - ? lib - : throw new DllNotFoundException($"The library {libPath} or one of its dependencies could not be found"); - } - - /// - /// Attempts to load the specified native libary into the current process by its name at runtime - /// - ///The path (or name of libary) to search for - /// - /// The used to search for libaries - /// within the current filesystem - /// - /// The handle to the libary if successfully loaded - /// True if the libary was found and loaded into the current process - public static bool TryLoadLibrary(string libPath, DllImportSearchPath searchPath, [NotNullWhen(true)] out SafeLibraryHandle? lib) - { - lib = null; - //Allow full rooted paths - if (Path.IsPathRooted(libPath)) - { - //Attempt a native load - if (NativeLibrary.TryLoad(libPath, out IntPtr libHandle)) - { - lib = new(libHandle); - return true; - } - return false; - } - //Check application directory first (including subdirectories) - if ((searchPath & DllImportSearchPath.ApplicationDirectory) > 0) - { - //get the current directory - string libDir = Directory.GetCurrentDirectory(); - if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib)) - { - return true; - } - } - //See if search in the calling assembly directory - if ((searchPath & DllImportSearchPath.AssemblyDirectory) > 0) - { - //Get the calling assmblies directory - string libDir = Assembly.GetCallingAssembly().Location; - Debug.WriteLine("Native library searching for calling assembly location:{0} ", libDir); - if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib)) - { - return true; - } - } - //Search system32 dir - if ((searchPath & DllImportSearchPath.System32) > 0) - { - //Get the system directory - string libDir = Environment.GetFolderPath(Environment.SpecialFolder.SystemX86); - if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib)) - { - return true; - } - } - //Attempt a native load - { - if (NativeLibrary.TryLoad(libPath, out IntPtr libHandle)) - { - lib = new(libHandle); - return true; - } - return false; - } - } - - private static bool TryLoadLibraryInternal(string libDir, string libPath, SearchOption dirSearchOptions, [NotNullWhen(true)] out SafeLibraryHandle? libary) - { - //Try to find the libary file - string? libFile = GetLibraryFile(libDir, libPath, dirSearchOptions); - //Load libary - if (libFile != null && NativeLibrary.TryLoad(libFile, out IntPtr libHandle)) - { - libary = new SafeLibraryHandle(libHandle); - return true; - } - libary = null; - return false; - } - - private static string? GetLibraryFile(string dirPath, string libPath, SearchOption search) - { - //If the library path already has an extension, just search for the file - if (Path.HasExtension(libPath)) - { - return Directory.EnumerateFiles(dirPath, libPath, search).FirstOrDefault(); - } - else - { - //slice the lib to its file name - libPath = Path.GetFileName(libPath); - libPath = Path.ChangeExtension(libPath, OperatingSystem.IsWindows() ? ".dll" : ".so"); - //Select the first file that matches the name - return Directory.EnumerateFiles(dirPath, libPath, search).FirstOrDefault(); - } - } + private SafeLibraryHandle(IntPtr libHandle, bool ownsHandle) : base(IntPtr.Zero, ownsHandle) => SetHandle(libHandle); /// /// Loads a native function pointer from the library of the specified name and @@ -179,7 +55,7 @@ namespace VNLib.Utils.Native { //Increment handle count before obtaining a method bool success = false; - DangerousAddRef(ref success); + DangerousAddRef(ref success); ObjectDisposedException.ThrowIf(success == false, this); @@ -218,40 +94,124 @@ namespace VNLib.Utils.Native return Marshal.GetDelegateForFunctionPointer(nativeMethod); } + /// + protected override bool ReleaseHandle() + { + //Free the library and set the handle as invalid + NativeLibrary.Free(handle); + SetHandleAsInvalid(); + return true; + } + /// - /// Loads a native method from the library of the specified name and managed delegate + /// Finds and loads the specified native libary into the current process by its name at runtime. + /// This function defaults to the executing assembly /// - /// The native method delegate type - /// The name of the native method - /// A wapper handle around the native method delegate + /// The path (or name of libary) to search for + /// + /// The used to search for libaries + /// within the current filesystem + /// + /// The loaded /// - /// If the handle is closed or invalid - /// When the specified entrypoint could not be found - [Obsolete("Updated naming, use GetFunction() instead")] - public SafeMethodHandle GetMethod(string methodName) where T : Delegate => GetFunction(methodName); + /// + public static SafeLibraryHandle LoadLibrary(string libPath, DllImportSearchPath searchPath = DllImportSearchPath.ApplicationDirectory) + { + //See if the path includes a file extension + return TryLoadLibrary(libPath, searchPath, out SafeLibraryHandle? lib) + ? lib + : throw new DllNotFoundException($"The library '{libPath}' or one of its dependencies could not be found"); + } /// - /// Gets an delegate wrapper for the specified method without tracking its referrence. - /// The caller must manage the referrence count in order - /// to not leak resources or cause process corruption + /// Finds and loads the specified native libary into the current process by its name at runtime /// - /// The native method delegate type - /// The name of the native method - /// A the delegate wrapper on the native method + /// The path (or name of libary) to search for + /// + /// The used to search for libaries + /// within the current filesystem + /// + /// The assembly loading the native library + /// The loaded /// - /// If the handle is closed or invalid - /// When the specified entrypoint could not be found - [Obsolete("Updated naming, use DangerousGetFunction() instead")] - public T DangerousGetMethod(string methodName) where T : Delegate => DangerousGetFunction(methodName); + /// + public static SafeLibraryHandle LoadLibrary( + string libPath, + Assembly assembly, + DllImportSearchPath searchPath = DllImportSearchPath.ApplicationDirectory + ) + { + //See if the path includes a file extension + return TryLoadLibrary(libPath, assembly, searchPath, out SafeLibraryHandle? lib) + ? lib + : throw new DllNotFoundException($"The library '{libPath}' or one of its dependencies could not be found"); + } + /// + /// Creates a new from an existing library pointer. + /// + /// A pointer to the existing (and loaded) library + /// A value that specifies whether the wrapper owns the library handle now + /// A safe library wrapper around the existing library pointer + /// + public unsafe static SafeLibraryHandle FromExisting(nint libHandle, bool ownsHandle) + { + ArgumentNullException.ThrowIfNull(libHandle.ToPointer(), nameof(libHandle)); + return new(libHandle, ownsHandle); + } - /// - protected override bool ReleaseHandle() + /// + /// Attempts to load the specified native libary into the current process by its name at runtime. + /// This function defaults to the executing assembly + /// + ///The path (or name of libary) to search for + /// + /// The used to search for libaries + /// within the current filesystem + /// + /// The handle to the libary if successfully loaded + /// True if the libary was found and loaded into the current process + public static bool TryLoadLibrary( + string libPath, + DllImportSearchPath searchPath, + [NotNullWhen(true)] out SafeLibraryHandle? library + ) { - //Free the library and set the handle as invalid - NativeLibrary.Free(handle); - SetHandleAsInvalid(); - return true; + return TryLoadLibrary( + libPath, + Assembly.GetExecutingAssembly(), //Use the executing assembly as the default loading assembly + searchPath, + out library + ); + } + + + + /// + /// Attempts to load the specified native libary into the current process by its name at runtime + /// + ///The path (or name of libary) to search for + /// + /// The used to search for libaries + /// within the current filesystem + /// + /// The handle to the libary if successfully loaded + /// The assembly loading the native library + /// True if the libary was found and loaded into the current process + /// + public static bool TryLoadLibrary( + string libPath, + Assembly assembly, + DllImportSearchPath searchPath, + [NotNullWhen(true)] out SafeLibraryHandle? library + ) + { + ArgumentNullException.ThrowIfNull(libPath); + ArgumentNullException.ThrowIfNull(assembly); + + NatveLibraryResolver resolver = new(libPath, assembly, searchPath); + + return resolver.ResolveAndLoadLibrary(out library); } } } diff --git a/lib/Utils/src/Resources/LazyInitializer.cs b/lib/Utils/src/Resources/LazyInitializer.cs new file mode 100644 index 0000000..5de43e0 --- /dev/null +++ b/lib/Utils/src/Resources/LazyInitializer.cs @@ -0,0 +1,112 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: LazyInitializer.cs +* +* LazyInitializer.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Threading; +using System.Diagnostics; + +namespace VNLib.Utils.Resources +{ + /// + /// A lazy initializer that creates a single instance of a type + /// and shares it across all threads. This class simply guarantees + /// that the instance is only created once and shared across all + /// threads as efficiently as possible for long running processes. + /// + /// + /// The callback function that initializes the instance + public sealed class LazyInitializer(Func initalizer) + { + private readonly object _lock = new(); + private readonly Func initalizer = initalizer ?? throw new ArgumentNullException(nameof(initalizer)); + + private T? _instance; + private bool _isLoaded; + + /// + /// A value indicating if the instance has ben loaded + /// + public bool IsLoaded => _isLoaded; + + /// + /// Gets or creates the instance only once and returns + /// the shared instance + /// + /// + /// NOTE: + /// Accessing this property may block the calling thread + /// if the instance has not yet been loaded. Only one thread + /// will create the instance, all other threads will wait + /// for the instance to be created. + /// + public T Instance + { + get + { + //See if instance is already loaded (this read is atomic in .NET) + if (_isLoaded) + { + return _instance!; + } + + /* + * Instance has not yet been loaded. Only one thread + * must load the object, all other threads must wait + * for the object to be loaded. + */ + + if (Monitor.TryEnter(_lock, 0)) + { + try + { + /* + * Lock was entered without waiting (lock was available), this will now be + * the thread that invokes the load function + */ + + _instance = initalizer(); + + //Finally set the load state + _isLoaded = true; + } + finally + { + Monitor.Exit(_lock); + } + } + else + { + //wait for lock to be released, when it is, the object should be loaded + Monitor.Enter(_lock); + Monitor.Exit(_lock); + + //object instance should now be available to non-creating threads + Debug.Assert(_isLoaded); + } + + return _instance!; + } + } + } +} \ No newline at end of file -- cgit