aboutsummaryrefslogtreecommitdiff
path: root/lib/Utils/src/Native
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-03-22 12:01:53 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2024-03-22 12:01:53 -0400
commit6c1667be23597513537f8190e2f55d65eb9b7c7a (patch)
tree380fe100af2b29246398237bfe95f392dc513ffe /lib/Utils/src/Native
parentebf688f2f974295beabf7b5def7e6f6f150551d0 (diff)
refactor: Overhauled native library loading and lazy init
Diffstat (limited to 'lib/Utils/src/Native')
-rw-r--r--lib/Utils/src/Native/NatveLibraryResolver.cs299
-rw-r--r--lib/Utils/src/Native/SafeLibraryHandle.cs260
2 files changed, 409 insertions, 150 deletions
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
+{
+ /// <summary>
+ /// Uses a supplied library state to resolve and load a platform native library
+ /// into the process memory space.
+ /// </summary>
+ /// <param name="suppliedLibraryPath">The raw caller-supplied library path to resolve</param>
+ /// <param name="assembly">A assembly loading the desired library</param>
+ /// <param name="searchPath">The dll loader search path requirements</param>
+ 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);
+
+ /// <summary>
+ /// Resolves and attempts to load the current library into the current process.
+ /// </summary>
+ /// <param name="library">The <see cref="SafeLibraryHandle"/> if the library was successfully resolved</param>
+ /// <returns>True if the library was resolved and loaded into the process, false otherwise</returns>
+ 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<string> 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<string> GetSpecialDirPaths() => SafeDirs.Select(Environment.GetFolderPath);
+
+ private static IEnumerable<string> 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
///<inheritdoc/>
public override bool IsInvalid => handle == IntPtr.Zero;
- private SafeLibraryHandle(IntPtr libHandle) : base(IntPtr.Zero, true)
- {
- //Init handle
- SetHandle(libHandle);
- }
-
- /// <summary>
- /// Finds and loads the specified native libary into the current process by its name at runtime
- /// </summary>
- /// <param name="libPath">The path (or name of libary) to search for</param>
- /// <param name="searchPath">
- /// The <see cref="DllImportSearchPath"/> used to search for libaries
- /// within the current filesystem
- /// </param>
- /// <returns>The loaded <see cref="SafeLibraryHandle"/></returns>
- /// <exception cref="ArgumentNullException"></exception>
- /// <exception cref="DllNotFoundException"></exception>
- public static SafeLibraryHandle LoadLibrary(string libPath, DllImportSearchPath searchPath = DllImportSearchPath.ApplicationDirectory)
- {
- 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");
- }
-
- /// <summary>
- /// Attempts to load the specified native libary into the current process by its name at runtime
- /// </summary>
- ///<param name="libPath">The path (or name of libary) to search for</param>
- /// <param name="searchPath">
- /// The <see cref="DllImportSearchPath"/> used to search for libaries
- /// within the current filesystem
- /// </param>
- /// <param name="lib">The handle to the libary if successfully loaded</param>
- /// <returns>True if the libary was found and loaded into the current process</returns>
- public static bool TryLoadLibrary(string libPath, DllImportSearchPath searchPath, [NotNullWhen(true)] out SafeLibraryHandle? lib)
- {
- lib = null;
- //Allow full rooted paths
- if (Path.IsPathRooted(libPath))
- {
- //Attempt a native load
- if (NativeLibrary.TryLoad(libPath, out IntPtr libHandle))
- {
- lib = new(libHandle);
- return true;
- }
- return false;
- }
- //Check application directory first (including subdirectories)
- if ((searchPath & DllImportSearchPath.ApplicationDirectory) > 0)
- {
- //get the current directory
- string libDir = Directory.GetCurrentDirectory();
- if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib))
- {
- return true;
- }
- }
- //See if search in the calling assembly directory
- if ((searchPath & DllImportSearchPath.AssemblyDirectory) > 0)
- {
- //Get the calling assmblies directory
- string libDir = Assembly.GetCallingAssembly().Location;
- Debug.WriteLine("Native library searching for calling assembly location:{0} ", libDir);
- if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib))
- {
- return true;
- }
- }
- //Search system32 dir
- if ((searchPath & DllImportSearchPath.System32) > 0)
- {
- //Get the system directory
- string libDir = Environment.GetFolderPath(Environment.SpecialFolder.SystemX86);
- if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib))
- {
- return true;
- }
- }
- //Attempt a native load
- {
- if (NativeLibrary.TryLoad(libPath, out IntPtr libHandle))
- {
- lib = new(libHandle);
- return true;
- }
- return false;
- }
- }
-
- private static bool TryLoadLibraryInternal(string libDir, string libPath, SearchOption dirSearchOptions, [NotNullWhen(true)] out SafeLibraryHandle? libary)
- {
- //Try to find the libary file
- string? libFile = GetLibraryFile(libDir, libPath, dirSearchOptions);
- //Load libary
- if (libFile != null && NativeLibrary.TryLoad(libFile, out IntPtr libHandle))
- {
- libary = new SafeLibraryHandle(libHandle);
- return true;
- }
- libary = null;
- return false;
- }
-
- private static string? GetLibraryFile(string dirPath, string libPath, SearchOption search)
- {
- //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);
/// <summary>
/// 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<T>(nativeMethod);
}
+ ///<inheritdoc/>
+ protected override bool ReleaseHandle()
+ {
+ //Free the library and set the handle as invalid
+ NativeLibrary.Free(handle);
+ SetHandleAsInvalid();
+ return true;
+ }
+
/// <summary>
- /// 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
/// </summary>
- /// <typeparam name="T">The native method delegate type</typeparam>
- /// <param name="methodName">The name of the native method</param>
- /// <returns>A wapper handle around the native method delegate</returns>
+ /// <param name="libPath">The path (or name of libary) to search for</param>
+ /// <param name="searchPath">
+ /// The <see cref="DllImportSearchPath"/> used to search for libaries
+ /// within the current filesystem
+ /// </param>
+ /// <returns>The loaded <see cref="SafeLibraryHandle"/></returns>
/// <exception cref="ArgumentNullException"></exception>
- /// <exception cref="ObjectDisposedException">If the handle is closed or invalid</exception>
- /// <exception cref="EntryPointNotFoundException">When the specified entrypoint could not be found</exception>
- [Obsolete("Updated naming, use GetFunction<T>() instead")]
- public SafeMethodHandle<T> GetMethod<T>(string methodName) where T : Delegate => GetFunction<T>(methodName);
+ /// <exception cref="DllNotFoundException"></exception>
+ 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");
+ }
/// <summary>
- /// Gets an delegate wrapper for the specified method without tracking its referrence.
- /// The caller must manage the <see cref="SafeLibraryHandle"/> referrence count in order
- /// to not leak resources or cause process corruption
+ /// Finds and loads the specified native libary into the current process by its name at runtime
/// </summary>
- /// <typeparam name="T">The native method delegate type</typeparam>
- /// <param name="methodName">The name of the native method</param>
- /// <returns>A the delegate wrapper on the native method</returns>
+ /// <param name="libPath">The path (or name of libary) to search for</param>
+ /// <param name="searchPath">
+ /// The <see cref="DllImportSearchPath"/> used to search for libaries
+ /// within the current filesystem
+ /// </param>
+ /// <param name="assembly">The assembly loading the native library</param>
+ /// <returns>The loaded <see cref="SafeLibraryHandle"/></returns>
/// <exception cref="ArgumentNullException"></exception>
- /// <exception cref="ObjectDisposedException">If the handle is closed or invalid</exception>
- /// <exception cref="EntryPointNotFoundException">When the specified entrypoint could not be found</exception>
- [Obsolete("Updated naming, use DangerousGetFunction<T>() instead")]
- public T DangerousGetMethod<T>(string methodName) where T : Delegate => DangerousGetFunction<T>(methodName);
+ /// <exception cref="DllNotFoundException"></exception>
+ 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");
+ }
+ /// <summary>
+ /// Creates a new <see cref="SafeLibraryHandle"/> from an existing library pointer.
+ /// </summary>
+ /// <param name="libHandle">A pointer to the existing (and loaded) library</param>
+ /// <param name="ownsHandle">A value that specifies whether the wrapper owns the library handle now</param>
+ /// <returns>A safe library wrapper around the existing library pointer</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public unsafe static SafeLibraryHandle FromExisting(nint libHandle, bool ownsHandle)
+ {
+ ArgumentNullException.ThrowIfNull(libHandle.ToPointer(), nameof(libHandle));
+ return new(libHandle, ownsHandle);
+ }
- ///<inheritdoc/>
- protected override bool ReleaseHandle()
+ /// <summary>
+ /// Attempts to load the specified native libary into the current process by its name at runtime.
+ /// This function defaults to the executing assembly
+ /// </summary>
+ ///<param name="libPath">The path (or name of libary) to search for</param>
+ /// <param name="searchPath">
+ /// The <see cref="DllImportSearchPath"/> used to search for libaries
+ /// within the current filesystem
+ /// </param>
+ /// <param name="library">The handle to the libary if successfully loaded</param>
+ /// <returns>True if the libary was found and loaded into the current process</returns>
+ public static bool TryLoadLibrary(
+ string libPath,
+ DllImportSearchPath searchPath,
+ [NotNullWhen(true)] out SafeLibraryHandle? 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
+ );
+ }
+
+
+
+ /// <summary>
+ /// Attempts to load the specified native libary into the current process by its name at runtime
+ /// </summary>
+ ///<param name="libPath">The path (or name of libary) to search for</param>
+ /// <param name="searchPath">
+ /// The <see cref="DllImportSearchPath"/> used to search for libaries
+ /// within the current filesystem
+ /// </param>
+ /// <param name="library">The handle to the libary if successfully loaded</param>
+ /// <param name="assembly">The assembly loading the native library</param>
+ /// <returns>True if the libary was found and loaded into the current process</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ 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);
}
}
}