diff options
Diffstat (limited to 'third-party/DotNetCorePlugins/src')
13 files changed, 1574 insertions, 0 deletions
diff --git a/third-party/DotNetCorePlugins/src/Internal/PlatformInformation.cs b/third-party/DotNetCorePlugins/src/Internal/PlatformInformation.cs new file mode 100644 index 0000000..0b4ac27 --- /dev/null +++ b/third-party/DotNetCorePlugins/src/Internal/PlatformInformation.cs @@ -0,0 +1,73 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +/* + * Modifications Copyright (c) 2024 Vaughn Nugent + * + * Changes: + * - Use the new .NET 8.0 collection syntax + * - Use the new OperatingSystem class for platform detection + * - Remove static constructor for new best practice guidlines + */ + +using System; +using System.Diagnostics; + +namespace McMaster.NETCore.Plugins +{ + internal class PlatformInformation + { + public static readonly string[] NativeLibraryExtensions = GetNativeLibExtension(); + public static readonly string[] NativeLibraryPrefixes = GetNativeLibPrefixs(); + + public static readonly string[] ManagedAssemblyExtensions = + [ + ".dll", + ".ni.dll", + ".exe", + ".ni.exe" + ]; + + private static string[] GetNativeLibPrefixs() + { + if (OperatingSystem.IsWindows()) + { + return [""]; + } + else if (OperatingSystem.IsMacOS()) + { + return ["", "lib",]; + } + else if (OperatingSystem.IsLinux()) + { + return ["", "lib"]; + } + else + { + Debug.Fail("Unknown OS type"); + return []; + } + } + + private 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/third-party/DotNetCorePlugins/src/Internal/RuntimeConfig.cs b/third-party/DotNetCorePlugins/src/Internal/RuntimeConfig.cs new file mode 100644 index 0000000..6da5dd4 --- /dev/null +++ b/third-party/DotNetCorePlugins/src/Internal/RuntimeConfig.cs @@ -0,0 +1,10 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace McMaster.NETCore.Plugins +{ + internal class RuntimeConfig + { + public RuntimeOptions? runtimeOptions { get; set; } + } +} diff --git a/third-party/DotNetCorePlugins/src/Internal/RuntimeOptions.cs b/third-party/DotNetCorePlugins/src/Internal/RuntimeOptions.cs new file mode 100644 index 0000000..748aa38 --- /dev/null +++ b/third-party/DotNetCorePlugins/src/Internal/RuntimeOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace McMaster.NETCore.Plugins +{ + internal class RuntimeOptions + { + public string? Tfm { get; set; } + + public string[]? AdditionalProbingPaths { get; set; } + } +} diff --git a/third-party/DotNetCorePlugins/src/LibraryModel/ManagedLibrary.cs b/third-party/DotNetCorePlugins/src/LibraryModel/ManagedLibrary.cs new file mode 100644 index 0000000..ca45cc5 --- /dev/null +++ b/third-party/DotNetCorePlugins/src/LibraryModel/ManagedLibrary.cs @@ -0,0 +1,73 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; + +namespace McMaster.NETCore.Plugins.LibraryModel +{ + /// <summary> + /// Represents a managed, .NET assembly. + /// </summary> + [DebuggerDisplay("{Name} = {AdditionalProbingPath}")] + public class ManagedLibrary + { + private ManagedLibrary(AssemblyName name, string additionalProbingPath, string appLocalPath) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath)); + AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath)); + } + + /// <summary> + /// Name of the managed library + /// </summary> + public AssemblyName Name { get; private set; } + + /// <summary> + /// Contains path to file within an additional probing path root. This is typically a combination + /// of the NuGet package ID (lowercased), version, and path within the package. + /// <para> + /// For example, <c>microsoft.data.sqlite/1.0.0/lib/netstandard1.3/Microsoft.Data.Sqlite.dll</c> + /// </para> + /// </summary> + public string AdditionalProbingPath { get; private set; } + + /// <summary> + /// Contains path to file within a deployed, framework-dependent application. + /// <para> + /// For most managed libraries, this will be the file name. + /// For example, <c>MyPlugin1.dll</c>. + /// </para> + /// <para> + /// For runtime-specific managed implementations, this may include a sub folder path. + /// For example, <c>runtimes/win/lib/netcoreapp2.0/System.Diagnostics.EventLog.dll</c> + /// </para> + /// </summary> + public string AppLocalPath { get; private set; } + + /// <summary> + /// Create an instance of <see cref="ManagedLibrary" /> from a NuGet package. + /// </summary> + /// <param name="packageId">The name of the package.</param> + /// <param name="packageVersion">The version of the package.</param> + /// <param name="assetPath">The path within the NuGet package.</param> + /// <returns></returns> + public static ManagedLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath) + { + // When the asset comes from "lib/$tfm/", Microsoft.NET.Sdk will flatten this during publish based on the most compatible TFM. + // The SDK will not flatten managed libraries found under runtimes/ + var appLocalPath = assetPath.StartsWith("lib/") + ? Path.GetFileName(assetPath) + : assetPath; + + return new ManagedLibrary( + new AssemblyName(Path.GetFileNameWithoutExtension(assetPath)), + Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath), + appLocalPath + ); + } + } +} diff --git a/third-party/DotNetCorePlugins/src/LibraryModel/NativeLibrary.cs b/third-party/DotNetCorePlugins/src/LibraryModel/NativeLibrary.cs new file mode 100644 index 0000000..ac5e061 --- /dev/null +++ b/third-party/DotNetCorePlugins/src/LibraryModel/NativeLibrary.cs @@ -0,0 +1,69 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; + +namespace McMaster.NETCore.Plugins.LibraryModel +{ + /// <summary> + /// Represents an unmanaged library, such as `libsqlite3`, which may need to be loaded + /// for P/Invoke to work. + /// </summary> + [DebuggerDisplay("{Name} = {AdditionalProbingPath}")] + public class NativeLibrary + { + private NativeLibrary(string name, string appLocalPath, string additionalProbingPath) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath)); + AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath)); + } + + /// <summary> + /// Name of the native library. This should match the name of the P/Invoke call. + /// <para> + /// For example, if specifying `[DllImport("sqlite3")]`, <see cref="Name" /> should be <c>sqlite3</c>. + /// This may not match the exact file name as loading will attempt variations on the name according + /// to OS convention. On Windows, P/Invoke will attempt to load `sqlite3.dll`. On macOS, it will + /// attempt to find `sqlite3.dylib` and `libsqlite3.dylib`. On Linux, it will attempt to find + /// `sqlite3.so` and `libsqlite3.so`. + /// </para> + /// </summary> + public string Name { get; private set; } + + /// <summary> + /// Contains path to file within a deployed, framework-dependent application + /// <para> + /// For example, <c>runtimes/linux-x64/native/libsqlite.so</c> + /// </para> + /// </summary> + public string AppLocalPath { get; private set; } + + /// <summary> + /// Contains path to file within an additional probing path root. This is typically a combination + /// of the NuGet package ID (lowercased), version, and path within the package. + /// <para> + /// For example, <c>sqlite/3.13.3/runtimes/linux-x64/native/libsqlite.so</c> + /// </para> + /// </summary> + public string AdditionalProbingPath { get; private set; } + + /// <summary> + /// Create an instance of <see cref="NativeLibrary" /> from a NuGet package. + /// </summary> + /// <param name="packageId">The name of the package.</param> + /// <param name="packageVersion">The version of the package.</param> + /// <param name="assetPath">The path within the NuGet package.</param> + /// <returns></returns> + public static NativeLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath) + { + return new NativeLibrary( + Path.GetFileNameWithoutExtension(assetPath), + assetPath, + Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath) + ); + } + } +} diff --git a/third-party/DotNetCorePlugins/src/Loader/AssemblyLoadContextBuilder.cs b/third-party/DotNetCorePlugins/src/Loader/AssemblyLoadContextBuilder.cs new file mode 100644 index 0000000..05d6238 --- /dev/null +++ b/third-party/DotNetCorePlugins/src/Loader/AssemblyLoadContextBuilder.cs @@ -0,0 +1,341 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +/* + * Modifications Copyright (c) 2024 Vaughn Nugent + * + * Changes: + * - Removed unloadable feature as an optional pragma (aka always on) + * - Removed unused + */ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Collections.Generic; + +using McMaster.NETCore.Plugins.LibraryModel; + +namespace McMaster.NETCore.Plugins.Loader +{ + /// <summary> + /// A builder for creating an instance of <see cref="AssemblyLoadContext" />. + /// </summary> + public class AssemblyLoadContextBuilder + { + private readonly List<string> _additionalProbingPaths = new(); + private readonly List<string> _resourceProbingPaths = new(); + private readonly List<string> _resourceProbingSubpaths = new(); + private readonly Dictionary<string, ManagedLibrary> _managedLibraries = new(StringComparer.Ordinal); + private readonly Dictionary<string, NativeLibrary> _nativeLibraries = new(StringComparer.Ordinal); + private readonly HashSet<string> _privateAssemblies = new(StringComparer.Ordinal); + private readonly HashSet<string> _defaultAssemblies = new(StringComparer.Ordinal); + private AssemblyLoadContext _defaultLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default; + private string? _mainAssemblyPath; + private bool _preferDefaultLoadContext; + private bool _isCollectible; + private bool _loadInMemory; + private bool _shadowCopyNativeLibraries; + + + /// <summary> + /// Creates an assembly load context using settings specified on the builder. + /// </summary> + /// <returns>A new ManagedLoadContext.</returns> + public AssemblyLoadContext Build() + { + var resourceProbingPaths = new List<string>(_resourceProbingPaths); + foreach (var additionalPath in _additionalProbingPaths) + { + foreach (var subPath in _resourceProbingSubpaths) + { + resourceProbingPaths.Add(Path.Combine(additionalPath, subPath)); + } + } + + if (_mainAssemblyPath == null) + { + throw new InvalidOperationException($"Missing required property. You must call '{nameof(SetMainAssemblyPath)}' to configure the default assembly."); + } + + //Lots of arguments, make it clear + return new ManagedLoadContext( + mainAssemblyPath: _mainAssemblyPath, + managedAssemblies: _managedLibraries, + nativeLibraries: _nativeLibraries, + privateAssemblies: _privateAssemblies, + defaultAssemblies: _defaultAssemblies, + additionalProbingPaths: _additionalProbingPaths, + resourceProbingPaths: resourceProbingPaths, + defaultLoadContext: _defaultLoadContext, + preferDefaultLoadContext: _preferDefaultLoadContext, + isCollectible: _isCollectible, + loadInMemory: _loadInMemory, + shadowCopyNativeLibraries: _shadowCopyNativeLibraries + ); + } + + /// <summary> + /// Set the file path to the main assembly for the context. This is used as the starting point for loading + /// other assemblies. The directory that contains it is also known as the 'app local' directory. + /// </summary> + /// <param name="path">The file path. Must not be null or empty. Must be an absolute path.</param> + /// <returns>The builder.</returns> + public AssemblyLoadContextBuilder SetMainAssemblyPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("Argument must not be null or empty.", nameof(path)); + } + + if (!Path.IsPathRooted(path)) + { + throw new ArgumentException("Argument must be a full path.", nameof(path)); + } + + _mainAssemblyPath = path; + return this; + } + + /// <summary> + /// Replaces the default <see cref="AssemblyLoadContext"/> used by the <see cref="AssemblyLoadContextBuilder"/>. + /// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context. + /// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/> + /// </summary> + /// <param name="context">The context to set.</param> + /// <returns>The builder.</returns> + public AssemblyLoadContextBuilder SetDefaultContext(AssemblyLoadContext context) + { + _defaultLoadContext = context ?? throw new ArgumentException($"Bad Argument: AssemblyLoadContext in {nameof(AssemblyLoadContextBuilder)}.{nameof(SetDefaultContext)} is null."); + return this; + } + + /// <summary> + /// Instructs the load context to prefer a private version of this assembly, even if that version is + /// different from the version used by the host application. + /// Use this when you do not need to exchange types created from within the load context with other contexts + /// or the default app context. + /// <para> + /// This may mean the types loaded from + /// this assembly will not match the types from an assembly with the same name, but different version, + /// in the host application. + /// </para> + /// <para> + /// For example, if the host application has a type named <c>Foo</c> from assembly <c>Banana, Version=1.0.0.0</c> + /// and the load context prefers a private version of <c>Banan, Version=2.0.0.0</c>, when comparing two objects, + /// one created by the host (Foo1) and one created from within the load context (Foo2), they will not have the same + /// type. <c>Foo1.GetType() != Foo2.GetType()</c> + /// </para> + /// </summary> + /// <param name="assemblyName">The name of the assembly.</param> + /// <returns>The builder.</returns> + public AssemblyLoadContextBuilder PreferLoadContextAssembly(AssemblyName assemblyName) + { + if (assemblyName.Name != null) + { + _privateAssemblies.Add(assemblyName.Name); + } + + return this; + } + + /// <summary> + /// Instructs the load context to first attempt to load assemblies by this name from the default app context, even + /// if other assemblies in this load context express a dependency on a higher or lower version. + /// Use this when you need to exchange types created from within the load context with other contexts + /// or the default app context. + /// </summary> + /// <param name="assemblyName">The name of the assembly.</param> + /// <returns>The builder.</returns> + public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName) + { + var names = new Queue<AssemblyName>(); + names.Enqueue(assemblyName); + while (names.TryDequeue(out var name)) + { + if (name.Name == null || _defaultAssemblies.Contains(name.Name)) + { + // base cases + continue; + } + + _defaultAssemblies.Add(name.Name); + + // Load and find all dependencies of default assemblies. + // This sacrifices some performance for determinism in how transitive + // dependencies will be shared between host and plugin. + var assembly = _defaultLoadContext.LoadFromAssemblyName(name); + + foreach (var reference in assembly.GetReferencedAssemblies()) + { + names.Enqueue(reference); + } + } + + return this; + } + + /// <summary> + /// Instructs the load context to first search for binaries from the default app context, even + /// if other assemblies in this load context express a dependency on a higher or lower version. + /// Use this when you need to exchange types created from within the load context with other contexts + /// or the default app context. + /// <para> + /// This may mean the types loaded from within the context are force-downgraded to the version provided + /// by the host. <seealso cref="PreferLoadContextAssembly" /> can be used to selectively identify binaries + /// which should not be loaded from the default load context. + /// </para> + /// </summary> + /// <param name="preferDefaultLoadContext">When true, first attemp to load binaries from the default load context.</param> + /// <returns>The builder.</returns> + public AssemblyLoadContextBuilder PreferDefaultLoadContext(bool preferDefaultLoadContext) + { + _preferDefaultLoadContext = preferDefaultLoadContext; + return this; + } + + + /// <summary> + /// Add a managed library to the load context. + /// </summary> + /// <param name="library">The managed library.</param> + /// <returns>The builder.</returns> + public AssemblyLoadContextBuilder AddManagedLibrary(ManagedLibrary library) + { + ValidateRelativePath(library.AdditionalProbingPath); + + if (library.Name.Name != null) + { + _managedLibraries.Add(library.Name.Name, library); + } + + return this; + } + + /// <summary> + /// Add a native library to the load context. + /// </summary> + /// <param name="library"></param> + /// <returns></returns> + public AssemblyLoadContextBuilder AddNativeLibrary(NativeLibrary library) + { + ValidateRelativePath(library.AppLocalPath); + ValidateRelativePath(library.AdditionalProbingPath); + + _nativeLibraries.Add(library.Name, library); + return this; + } + + /// <summary> + /// Add a <paramref name="path"/> that should be used to search for native and managed libraries. + /// </summary> + /// <param name="path">The file path. Must be a full file path.</param> + /// <returns>The builder</returns> + public AssemblyLoadContextBuilder AddProbingPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("Value must not be null or empty.", nameof(path)); + } + + if (!Path.IsPathRooted(path)) + { + throw new ArgumentException("Argument must be a full path.", nameof(path)); + } + + _additionalProbingPaths.Add(path); + return this; + } + + /// <summary> + /// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies). + /// </summary> + /// <param name="path">The file path. Must be a full file path.</param> + /// <returns>The builder</returns> + public AssemblyLoadContextBuilder AddResourceProbingPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("Value must not be null or empty.", nameof(path)); + } + + if (!Path.IsPathRooted(path)) + { + throw new ArgumentException("Argument must be a full path.", nameof(path)); + } + + _resourceProbingPaths.Add(path); + return this; + } + + + /// <summary> + /// Enable unloading the assembly load context. + /// </summary> + /// <returns>The builder</returns> + public AssemblyLoadContextBuilder EnableUnloading() + { + _isCollectible = true; + return this; + } + + /// <summary> + /// Read .dll files into memory to avoid locking the files. + /// This is not as efficient, so is not enabled by default, but is required for scenarios + /// like hot reloading. + /// </summary> + /// <returns>The builder</returns> + public AssemblyLoadContextBuilder PreloadAssembliesIntoMemory() + { + _loadInMemory = true; // required to prevent dotnet from locking loaded files + return this; + } + + /// <summary> + /// Shadow copy native libraries (unmanaged DLLs) to avoid locking of these files. + /// This is not as efficient, so is not enabled by default, but is required for scenarios + /// like hot reloading of plugins dependent on native libraries. + /// </summary> + /// <returns>The builder</returns> + public AssemblyLoadContextBuilder ShadowCopyNativeLibraries() + { + _shadowCopyNativeLibraries = true; + return this; + } + + /// <summary> + /// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies) + /// relative to any paths specified as <see cref="AddProbingPath"/> + /// </summary> + /// <param name="path">The file path. Must not be a full file path since it will be appended to additional probing path roots.</param> + /// <returns>The builder</returns> + internal AssemblyLoadContextBuilder AddResourceProbingSubpath(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("Value must not be null or empty.", nameof(path)); + } + + if (Path.IsPathRooted(path)) + { + throw new ArgumentException("Argument must be not a full path.", nameof(path)); + } + + _resourceProbingSubpaths.Add(path); + return this; + } + + private static void ValidateRelativePath(string probingPath) + { + if (string.IsNullOrEmpty(probingPath)) + { + throw new ArgumentException("Value must not be null or empty.", nameof(probingPath)); + } + + if (Path.IsPathRooted(probingPath)) + { + throw new ArgumentException("Argument must be a relative path.", nameof(probingPath)); + } + } + } +} diff --git a/third-party/DotNetCorePlugins/src/Loader/DependencyContextExtensions.cs b/third-party/DotNetCorePlugins/src/Loader/DependencyContextExtensions.cs new file mode 100644 index 0000000..fc2d0a9 --- /dev/null +++ b/third-party/DotNetCorePlugins/src/Loader/DependencyContextExtensions.cs @@ -0,0 +1,207 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using McMaster.NETCore.Plugins.LibraryModel; +using Microsoft.Extensions.DependencyModel; +using NativeLibrary = McMaster.NETCore.Plugins.LibraryModel.NativeLibrary; + +namespace McMaster.NETCore.Plugins.Loader +{ + /// <summary> + /// Extensions for configuring a load context using .deps.json files. + /// </summary> + public static class DependencyContextExtensions + { + /// <summary> + /// Add dependency information to a load context from a .deps.json file. + /// </summary> + /// <param name="builder">The builder.</param> + /// <param name="depsFilePath">The full path to the .deps.json file.</param> + /// <param name="error">An error, if one occurs while reading .deps.json</param> + /// <returns>The builder.</returns> + public static AssemblyLoadContextBuilder TryAddDependencyContext(this AssemblyLoadContextBuilder builder, string depsFilePath, out Exception? error) + { + error = null; + try + { + builder.AddDependencyContext(depsFilePath); + } + catch (Exception ex) + { + error = ex; + } + + return builder; + } + + /// <summary> + /// Add dependency information to a load context from a .deps.json file. + /// </summary> + /// <param name="builder">The builder.</param> + /// <param name="depsFilePath">The full path to the .deps.json file.</param> + /// <returns>The builder.</returns> + public static AssemblyLoadContextBuilder AddDependencyContext(this AssemblyLoadContextBuilder builder, string depsFilePath) + { + + var reader = new DependencyContextJsonReader(); + using (var file = File.OpenRead(depsFilePath)) + { + var deps = reader.Read(file); + builder.AddDependencyContext(deps); + } + + return builder; + } + + private static string GetFallbackRid() + { + // see https://github.com/dotnet/core-setup/blob/b64f7fffbd14a3517186b9a9d5cc001ab6e5bde6/src/corehost/common/pal.h#L53-L73 + + string ridBase; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + ridBase = "win10"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + ridBase = "linux"; + + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + ridBase = "osx.10.12"; + } + else + { + return "any"; + } + + return RuntimeInformation.OSArchitecture switch + { + Architecture.X86 => ridBase + "-x86", + Architecture.X64 => ridBase + "-x64", + Architecture.Arm => ridBase + "-arm", + Architecture.Arm64 => ridBase + "-arm64", + _ => ridBase, + }; + } + + /// <summary> + /// Add a pre-parsed <see cref="DependencyContext" /> to the load context. + /// </summary> + /// <param name="builder">The builder.</param> + /// <param name="dependencyContext">The dependency context.</param> + /// <returns>The builder.</returns> + public static AssemblyLoadContextBuilder AddDependencyContext(this AssemblyLoadContextBuilder builder, DependencyContext dependencyContext) + { + var ridGraph = dependencyContext.RuntimeGraph.Any() || DependencyContext.Default == null + ? dependencyContext.RuntimeGraph + : DependencyContext.Default.RuntimeGraph; + + var rid = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.GetRuntimeIdentifier(); + var fallbackRid = GetFallbackRid(); + var fallbackGraph = ridGraph.FirstOrDefault(g => g.Runtime == rid) + ?? ridGraph.FirstOrDefault(g => g.Runtime == fallbackRid) + ?? new RuntimeFallbacks("any"); + + foreach (var managed in dependencyContext.ResolveRuntimeAssemblies(fallbackGraph)) + { + builder.AddManagedLibrary(managed); + } + + foreach (var library in dependencyContext.ResolveResourceAssemblies()) + { + foreach (var resource in library.ResourceAssemblies) + { + /* + * For resource assemblies, look in $packageRoot/$packageId/$version/$resourceGrandparent + * + * For example, a deps file may contain + * + * "Example/1.0.0": { + * "runtime": { + * "lib/netcoreapp2.0/Example.dll": { } + * }, + * "resources": { + * "lib/netcoreapp2.0/es/Example.resources.dll": { + * "locale": "es" + * } + * } + * } + * + * In this case, probing should happen in $packageRoot/example/1.0.0/lib/netcoreapp2.0 + */ + + var resourceDir = Path.GetDirectoryName(Path.GetDirectoryName(resource.Path)); + + if (resourceDir != null) + { + var path = Path.Combine(library.Name.ToLowerInvariant(), + library.Version, + resourceDir); + + builder.AddResourceProbingSubpath(path); + } + } + } + + foreach (var native in dependencyContext.ResolveNativeAssets(fallbackGraph)) + { + builder.AddNativeLibrary(native); + } + + return builder; + } + + private static IEnumerable<ManagedLibrary> ResolveRuntimeAssemblies(this DependencyContext depContext, RuntimeFallbacks runtimeGraph) + { + var rids = GetRids(runtimeGraph); + return from library in depContext.RuntimeLibraries + from assetPath in SelectAssets(rids, library.RuntimeAssemblyGroups) + select ManagedLibrary.CreateFromPackage(library.Name, library.Version, assetPath); + } + + private static IEnumerable<RuntimeLibrary> ResolveResourceAssemblies(this DependencyContext depContext) + { + return from library in depContext.RuntimeLibraries + where library.ResourceAssemblies != null && library.ResourceAssemblies.Count > 0 + select library; + } + + private static IEnumerable<NativeLibrary> ResolveNativeAssets(this DependencyContext depContext, RuntimeFallbacks runtimeGraph) + { + var rids = GetRids(runtimeGraph); + return from library in depContext.RuntimeLibraries + from assetPath in SelectAssets(rids, library.NativeLibraryGroups) + // some packages include symbols alongside native assets, such as System.Native.a or pwshplugin.pdb + where PlatformInformation.NativeLibraryExtensions.Contains(Path.GetExtension(assetPath), StringComparer.OrdinalIgnoreCase) + select NativeLibrary.CreateFromPackage(library.Name, library.Version, assetPath); + } + + private static IEnumerable<string> GetRids(RuntimeFallbacks runtimeGraph) + { + return new[] { runtimeGraph.Runtime }.Concat(runtimeGraph?.Fallbacks ?? Enumerable.Empty<string>()); + } + + private static IEnumerable<string> SelectAssets(IEnumerable<string> rids, IEnumerable<RuntimeAssetGroup> groups) + { + foreach (var rid in rids) + { + var group = groups.FirstOrDefault(g => g.Runtime == rid); + if (group != null) + { + return group.AssetPaths; + } + } + + // Return the RID-agnostic group + return groups.GetDefaultAssets(); + } + } +} diff --git a/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs b/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs new file mode 100644 index 0000000..01b985e --- /dev/null +++ b/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs @@ -0,0 +1,386 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +/* + * Modifications Copyright (c) 2024 Vaughn Nugent + * + * Changes: + * - Removed lazy loading and hot-reload features + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; + +using McMaster.NETCore.Plugins.LibraryModel; + +namespace McMaster.NETCore.Plugins.Loader +{ + /// <summary> + /// An implementation of <see cref="AssemblyLoadContext" /> which attempts to load managed and native + /// binaries at runtime immitating some of the behaviors of corehost. + /// </summary> + [DebuggerDisplay("'{Name}' ({_mainAssemblyPath})")] + internal class ManagedLoadContext : AssemblyLoadContext + { + private readonly string _basePath; + private readonly string _mainAssemblyPath; + private readonly IReadOnlyDictionary<string, ManagedLibrary> _managedAssemblies; + private readonly IReadOnlyDictionary<string, NativeLibrary> _nativeLibraries; + private readonly IReadOnlyCollection<string> _privateAssemblies; + private readonly ICollection<string> _defaultAssemblies; + private readonly IReadOnlyCollection<string> _additionalProbingPaths; + private readonly bool _preferDefaultLoadContext; + private readonly string[] _resourceRoots; + private readonly bool _loadInMemory; + private readonly AssemblyLoadContext _defaultLoadContext; + private readonly AssemblyDependencyResolver _dependencyResolver; + private readonly bool _shadowCopyNativeLibraries; + private readonly string _unmanagedDllShadowCopyDirectoryPath; + + public ManagedLoadContext(string mainAssemblyPath, + IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies, + IReadOnlyDictionary<string, NativeLibrary> nativeLibraries, + IReadOnlyCollection<string> privateAssemblies, + IReadOnlyCollection<string> defaultAssemblies, + IReadOnlyCollection<string> additionalProbingPaths, + IReadOnlyCollection<string> resourceProbingPaths, + AssemblyLoadContext defaultLoadContext, + bool preferDefaultLoadContext, + bool isCollectible, + bool loadInMemory, + bool shadowCopyNativeLibraries) + : base(Path.GetFileNameWithoutExtension(mainAssemblyPath), isCollectible) + + { + ArgumentNullException.ThrowIfNull(resourceProbingPaths); + + _mainAssemblyPath = mainAssemblyPath ?? throw new ArgumentNullException(nameof(mainAssemblyPath)); + _dependencyResolver = new AssemblyDependencyResolver(mainAssemblyPath); + _basePath = Path.GetDirectoryName(mainAssemblyPath) ?? throw new ArgumentException(nameof(mainAssemblyPath)); + _managedAssemblies = managedAssemblies ?? throw new ArgumentNullException(nameof(managedAssemblies)); + _privateAssemblies = privateAssemblies ?? throw new ArgumentNullException(nameof(privateAssemblies)); + _defaultAssemblies = defaultAssemblies != null ? defaultAssemblies.ToList() : throw new ArgumentNullException(nameof(defaultAssemblies)); + _nativeLibraries = nativeLibraries ?? throw new ArgumentNullException(nameof(nativeLibraries)); + _additionalProbingPaths = additionalProbingPaths ?? throw new ArgumentNullException(nameof(additionalProbingPaths)); + _defaultLoadContext = defaultLoadContext; + _preferDefaultLoadContext = preferDefaultLoadContext; + _loadInMemory = loadInMemory; + + _resourceRoots = new[] { _basePath } + .Concat(resourceProbingPaths) + .ToArray(); + + _shadowCopyNativeLibraries = shadowCopyNativeLibraries; + _unmanagedDllShadowCopyDirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + if (shadowCopyNativeLibraries) + { + Unloading += _ => OnUnloaded(); + } + } + + /// <summary> + /// Load an assembly. + /// </summary> + /// <param name="assemblyName"></param> + /// <returns></returns> + protected override Assembly? Load(AssemblyName assemblyName) + { + if (assemblyName.Name == null) + { + // not sure how to handle this case. It's technically possible. + return null; + } + + if ((_preferDefaultLoadContext || _defaultAssemblies.Contains(assemblyName.Name)) && !_privateAssemblies.Contains(assemblyName.Name)) + { + // If default context is preferred, check first for types in the default context unless the dependency has been declared as private + try + { + var defaultAssembly = _defaultLoadContext.LoadFromAssemblyName(assemblyName); + if (defaultAssembly != null) + { + // Older versions used to return null here such that returned assembly would be resolved from the default ALC. + // However, with the addition of custom default ALCs, the Default ALC may not be the user's chosen ALC when + // this context was built. As such, we simply return the Assembly from the user's chosen default load context. + return defaultAssembly; + } + } + catch + { + // Swallow errors in loading from the default context + } + } + + var resolvedPath = _dependencyResolver.ResolveAssemblyToPath(assemblyName); + if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath)) + { + return LoadAssemblyFromFilePath(resolvedPath); + } + + // Resource assembly binding does not use the TPA. Instead, it probes PLATFORM_RESOURCE_ROOTS (a list of folders) + // for $folder/$culture/$assemblyName.dll + // See https://github.com/dotnet/coreclr/blob/3fca50a36e62a7433d7601d805d38de6baee7951/src/binder/assemblybinder.cpp#L1232-L1290 + + if (!string.IsNullOrEmpty(assemblyName.CultureName) && !string.Equals("neutral", assemblyName.CultureName)) + { + foreach (var resourceRoot in _resourceRoots) + { + var resourcePath = Path.Combine(resourceRoot, assemblyName.CultureName, assemblyName.Name + ".dll"); + if (File.Exists(resourcePath)) + { + return LoadAssemblyFromFilePath(resourcePath); + } + } + + return null; + } + + if (_managedAssemblies.TryGetValue(assemblyName.Name, out var library) && library != null) + { + if (SearchForLibrary(library, out var path) && path != null) + { + return LoadAssemblyFromFilePath(path); + } + } + else + { + // if an assembly was not listed in the list of known assemblies, + // fallback to the load context base directory + var dllName = assemblyName.Name + ".dll"; + foreach (var probingPath in _additionalProbingPaths.Prepend(_basePath)) + { + var localFile = Path.Combine(probingPath, dllName); + if (File.Exists(localFile)) + { + return LoadAssemblyFromFilePath(localFile); + } + } + } + + return null; + } + + public Assembly LoadAssemblyFromFilePath(string path) + { + if (!_loadInMemory) + { + return LoadFromAssemblyPath(path); + } + + using var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read); + var pdbPath = Path.ChangeExtension(path, ".pdb"); + if (File.Exists(pdbPath)) + { + using var pdbFile = File.Open(pdbPath, FileMode.Open, FileAccess.Read, FileShare.Read); + return LoadFromStream(file, pdbFile); + } + return LoadFromStream(file); + + } + + /// <summary> + /// Loads the unmanaged binary using configured list of native libraries. + /// </summary> + /// <param name="unmanagedDllName"></param> + /// <returns></returns> + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var resolvedPath = _dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath)) + { + return LoadUnmanagedDllFromResolvedPath(resolvedPath, normalizePath: false); + } + + foreach (var prefix in PlatformInformation.NativeLibraryPrefixes) + { + if (_nativeLibraries.TryGetValue(prefix + unmanagedDllName, out var library)) + { + if (SearchForLibrary(library, prefix, out var path) && path != null) + { + return LoadUnmanagedDllFromResolvedPath(path); + } + } + else + { + // coreclr allows code to use [DllImport("sni")] or [DllImport("sni.dll")] + // This library treats the file name without the extension as the lookup name, + // so this loop is necessary to check if the unmanaged name matches a library + // when the file extension has been trimmed. + foreach (var suffix in PlatformInformation.NativeLibraryExtensions) + { + if (!unmanagedDllName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // check to see if there is a library entry for the library without the file extension + var trimmedName = unmanagedDllName.Substring(0, unmanagedDllName.Length - suffix.Length); + + if (_nativeLibraries.TryGetValue(prefix + trimmedName, out library)) + { + if (SearchForLibrary(library, prefix, out var path) && path != null) + { + return LoadUnmanagedDllFromResolvedPath(path); + } + } + else + { + // fallback to native assets which match the file name in the plugin base directory + var prefixSuffixDllName = prefix + unmanagedDllName + suffix; + var prefixDllName = prefix + unmanagedDllName; + + foreach (var probingPath in _additionalProbingPaths.Prepend(_basePath)) + { + var localFile = Path.Combine(probingPath, prefixSuffixDllName); + if (File.Exists(localFile)) + { + return LoadUnmanagedDllFromResolvedPath(localFile); + } + + var localFileWithoutSuffix = Path.Combine(probingPath, prefixDllName); + if (File.Exists(localFileWithoutSuffix)) + { + return LoadUnmanagedDllFromResolvedPath(localFileWithoutSuffix); + } + } + + } + } + + } + } + + return base.LoadUnmanagedDll(unmanagedDllName); + } + + private bool SearchForLibrary(ManagedLibrary library, out string? path) + { + // 1. Check for in _basePath + app local path + var localFile = Path.Combine(_basePath, library.AppLocalPath); + if (File.Exists(localFile)) + { + path = localFile; + return true; + } + + // 2. Search additional probing paths + foreach (var searchPath in _additionalProbingPaths) + { + var candidate = Path.Combine(searchPath, library.AdditionalProbingPath); + if (File.Exists(candidate)) + { + path = candidate; + return true; + } + } + + // 3. Search in base path + foreach (var ext in PlatformInformation.ManagedAssemblyExtensions) + { + var local = Path.Combine(_basePath, library.Name.Name + ext); + if (File.Exists(local)) + { + path = local; + return true; + } + } + + path = null; + return false; + } + + private bool SearchForLibrary(NativeLibrary library, string prefix, out string? path) + { + // 1. Search in base path + foreach (var ext in PlatformInformation.NativeLibraryExtensions) + { + var candidate = Path.Combine(_basePath, $"{prefix}{library.Name}{ext}"); + if (File.Exists(candidate)) + { + path = candidate; + return true; + } + } + + // 2. Search in base path + app local (for portable deployments of netcoreapp) + var local = Path.Combine(_basePath, library.AppLocalPath); + if (File.Exists(local)) + { + path = local; + return true; + } + + // 3. Search additional probing paths + foreach (var searchPath in _additionalProbingPaths) + { + var candidate = Path.Combine(searchPath, library.AdditionalProbingPath); + if (File.Exists(candidate)) + { + path = candidate; + return true; + } + } + + path = null; + return false; + } + + private IntPtr LoadUnmanagedDllFromResolvedPath(string unmanagedDllPath, bool normalizePath = true) + { + if (normalizePath) + { + unmanagedDllPath = Path.GetFullPath(unmanagedDllPath); + } + + return _shadowCopyNativeLibraries + ? LoadUnmanagedDllFromShadowCopy(unmanagedDllPath) + : LoadUnmanagedDllFromPath(unmanagedDllPath); + } + + private IntPtr LoadUnmanagedDllFromShadowCopy(string unmanagedDllPath) + { + var shadowCopyDllPath = CreateShadowCopy(unmanagedDllPath); + + return LoadUnmanagedDllFromPath(shadowCopyDllPath); + } + + private string CreateShadowCopy(string dllPath) + { + Directory.CreateDirectory(_unmanagedDllShadowCopyDirectoryPath); + + var dllFileName = Path.GetFileName(dllPath); + var shadowCopyPath = Path.Combine(_unmanagedDllShadowCopyDirectoryPath, dllFileName); + + if (!File.Exists(shadowCopyPath)) + { + File.Copy(dllPath, shadowCopyPath); + } + + return shadowCopyPath; + } + + private void OnUnloaded() + { + if (!_shadowCopyNativeLibraries || !Directory.Exists(_unmanagedDllShadowCopyDirectoryPath)) + { + return; + } + + // Attempt to delete shadow copies + try + { + Directory.Delete(_unmanagedDllShadowCopyDirectoryPath, recursive: true); + } + catch (Exception) + { + // Files might be locked by host process. Nothing we can do about it, I guess. + } + } + } +} diff --git a/third-party/DotNetCorePlugins/src/Loader/RuntimeConfigExtensions.cs b/third-party/DotNetCorePlugins/src/Loader/RuntimeConfigExtensions.cs new file mode 100644 index 0000000..679c56a --- /dev/null +++ b/third-party/DotNetCorePlugins/src/Loader/RuntimeConfigExtensions.cs @@ -0,0 +1,129 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.Json; + +namespace McMaster.NETCore.Plugins.Loader +{ + /// <summary> + /// Extensions for creating a load context using settings from a runtimeconfig.json file + /// </summary> + public static class RuntimeConfigExtensions + { + private const string JsonExt = ".json"; + private static readonly JsonSerializerOptions s_serializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + /// <summary> + /// Adds additional probing paths to a managed load context using settings found in the runtimeconfig.json + /// and runtimeconfig.dev.json files. + /// </summary> + /// <param name="builder">The context builder</param> + /// <param name="runtimeConfigPath">The path to the runtimeconfig.json file</param> + /// <param name="includeDevConfig">Also read runtimeconfig.dev.json file, if present.</param> + /// <param name="error">The error, if one occurs while parsing runtimeconfig.json</param> + /// <returns>The builder.</returns> + public static AssemblyLoadContextBuilder TryAddAdditionalProbingPathFromRuntimeConfig( + this AssemblyLoadContextBuilder builder, + string runtimeConfigPath, + bool includeDevConfig, + out Exception? error) + { + error = null; + try + { + var config = TryReadConfig(runtimeConfigPath); + if (config == null) + { + return builder; + } + + RuntimeConfig? devConfig = null; + if (includeDevConfig) + { + var configDevPath = runtimeConfigPath.Substring(0, runtimeConfigPath.Length - JsonExt.Length) + ".dev.json"; + devConfig = TryReadConfig(configDevPath); + } + + var tfm = config.runtimeOptions?.Tfm ?? devConfig?.runtimeOptions?.Tfm; + + if (config.runtimeOptions != null) + { + AddProbingPaths(builder, config.runtimeOptions, tfm); + } + + if (devConfig?.runtimeOptions != null) + { + AddProbingPaths(builder, devConfig.runtimeOptions, tfm); + } + + if (tfm != null) + { + var dotnet = Process.GetCurrentProcess().MainModule.FileName; + if (string.Equals(Path.GetFileNameWithoutExtension(dotnet), "dotnet", StringComparison.OrdinalIgnoreCase)) + { + var dotnetHome = Path.GetDirectoryName(dotnet); + if (dotnetHome != null) + { + builder.AddProbingPath(Path.Combine(dotnetHome, "store", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), tfm)); + } + } + } + } + catch (Exception ex) + { + error = ex; + } + return builder; + } + + private static void AddProbingPaths(AssemblyLoadContextBuilder builder, RuntimeOptions options, string? tfm) + { + if (options.AdditionalProbingPaths == null) + { + return; + } + + foreach (var item in options.AdditionalProbingPaths) + { + var path = item; + if (path.Contains("|arch|")) + { + path = path.Replace("|arch|", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant()); + } + + if (path.Contains("|tfm|")) + { + if (tfm == null) + { + // We don't have enough information to parse this + continue; + } + + path = path.Replace("|tfm|", tfm); + } + + builder.AddProbingPath(path); + } + } + + private static RuntimeConfig? TryReadConfig(string path) + { + try + { + var file = File.ReadAllBytes(path); + return JsonSerializer.Deserialize<RuntimeConfig>(file, s_serializerOptions); + } + catch + { + return null; + } + } + } +} diff --git a/third-party/DotNetCorePlugins/src/McMaster.NETCore.Plugins.csproj b/third-party/DotNetCorePlugins/src/McMaster.NETCore.Plugins.csproj new file mode 100644 index 0000000..fbc293c --- /dev/null +++ b/third-party/DotNetCorePlugins/src/McMaster.NETCore.Plugins.csproj @@ -0,0 +1,26 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>net8.0</TargetFrameworks> + <OutputType>library</OutputType> + <Nullable>enable</Nullable> + <IsPublishable>False</IsPublishable> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageDescription>Provides API for dynamically loading assemblies into a .NET application. + +This package should be used by the host application which needs to load plugins. +See https://github.com/natemcmaster/DotNetCorePlugins/blob/main/README.md for more samples and documentation. + </PackageDescription> + <PackageTags>.NET Core;plugins</PackageTags> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\lib\Utils\src\VNLib.Utils.csproj" /> + </ItemGroup> + +</Project> diff --git a/third-party/DotNetCorePlugins/src/PluginConfig.cs b/third-party/DotNetCorePlugins/src/PluginConfig.cs new file mode 100644 index 0000000..d9f781c --- /dev/null +++ b/third-party/DotNetCorePlugins/src/PluginConfig.cs @@ -0,0 +1,94 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +/* + * Modifications Copyright (c) 2024 Vaughn Nugent + * + * Changes: + * - Removed unloadable feature as an optional pragma (aka always on) + * - Removed hot reload as an option + * - Used net 8.0 auto properties instead of field indexers + * - Removed experimental lazy loading since it's not safe for my use cases + */ + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Collections.Generic; + +namespace McMaster.NETCore.Plugins +{ + /// <summary> + /// Represents the configuration for a .NET Core plugin. + /// </summary> + public class PluginConfig + { + /// <summary> + /// Initializes a new instance of <see cref="PluginConfig" /> + /// </summary> + /// <param name="mainAssemblyPath">The full file path to the main assembly for the plugin.</param> + public PluginConfig(string mainAssemblyPath) + { + if (string.IsNullOrEmpty(mainAssemblyPath)) + { + throw new ArgumentException("Value must be null or not empty", nameof(mainAssemblyPath)); + } + + if (!Path.IsPathRooted(mainAssemblyPath)) + { + throw new ArgumentException("Value must be an absolute file path", nameof(mainAssemblyPath)); + } + + MainAssemblyPath = mainAssemblyPath; + } + + /// <summary> + /// The file path to the main assembly. + /// </summary> + public string MainAssemblyPath { get; } + + /// <summary> + /// A list of assemblies which should be treated as private. + /// </summary> + public ICollection<AssemblyName> PrivateAssemblies { get; protected set; } = new List<AssemblyName>(); + + /// <summary> + /// A list of assemblies which should be unified between the host and the plugin. + /// </summary> + /// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md"> + /// https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md + /// </seealso> + public ICollection<AssemblyName> SharedAssemblies { get; protected set; } = new List<AssemblyName>(); + + /// <summary> + /// Attempt to unify all types from a plugin with the host. + /// <para> + /// This does not guarantee types will unify. + /// </para> + /// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md"> + /// https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md + /// </seealso> + /// </summary> + public bool PreferSharedTypes { get; set; } + + /// <summary> + /// If set, replaces the default <see cref="AssemblyLoadContext"/> used by the <see cref="PluginLoader"/>. + /// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context. + /// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/> + /// </summary> + public AssemblyLoadContext DefaultContext { get; set; } = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default; + + /// <summary> + /// The plugin can be unloaded from memory. + /// </summary> + public bool IsUnloadable { get; set; } + + /// <summary> + /// Loads assemblies into memory in order to not lock files. + /// As example use case here would be: no hot reloading but able to + /// replace files and reload manually at later time + /// </summary> + public bool LoadInMemory { get; set; } + } +} diff --git a/third-party/DotNetCorePlugins/src/PluginLoader.cs b/third-party/DotNetCorePlugins/src/PluginLoader.cs new file mode 100644 index 0000000..cbe46f8 --- /dev/null +++ b/third-party/DotNetCorePlugins/src/PluginLoader.cs @@ -0,0 +1,148 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +/* + * Modifications Copyright (c) 2024 Vaughn Nugent + * + * Changes: + * - Removed unloadable feature as an optional pragma (aka always on) + * - Removed internal hot-reload since my plugin runtime handles this feature better + * - Removed all static creation functions as plugin config is far more direct and simple + * - Removed old/depricated features such as native deps resolution since thats handled now + * - Remove experimenal lazy loading + * - Only store the main asm file path instead of the mutable config instance + */ + +using System; +using System.Reflection; +using System.Runtime.Loader; + +using McMaster.NETCore.Plugins.Loader; + +namespace McMaster.NETCore.Plugins +{ + /// <summary> + /// This loader attempts to load binaries for execution (both managed assemblies and native libraries) + /// in the same way that .NET Core would if they were originally part of the .NET Core application. + /// <para> + /// This loader reads configuration files produced by .NET Core (.deps.json and runtimeconfig.json) + /// as well as a custom file (*.config files). These files describe a list of .dlls and a set of dependencies. + /// The loader searches the plugin path, as well as any additionally specified paths, for binaries + /// which satisfy the plugin's requirements. + /// </para> + /// </summary> + public class PluginLoader + { + + private readonly string _mainAssemblyPath; + private readonly AssemblyLoadContextBuilder _contextBuilder; + + private ManagedLoadContext _context; + + /// <summary> + /// Initialize an instance of <see cref="PluginLoader" /> + /// </summary> + /// <param name="config">The configuration for the plugin.</param> + public PluginLoader(PluginConfig config) + { + ArgumentNullException.ThrowIfNull(config); + ArgumentException.ThrowIfNullOrWhiteSpace(config.MainAssemblyPath); + + _mainAssemblyPath = config.MainAssemblyPath; + _contextBuilder = CreateLoadContextBuilder(config); + } + + /// <summary> + /// True when this plugin is capable of being unloaded. + /// </summary> + public bool IsUnloadable => _context.IsCollectible; + + /// <summary> + /// Initializes the new load context + /// </summary> + public void Load() => _context = (ManagedLoadContext)_contextBuilder.Build(); + + internal AssemblyLoadContext LoadContext => _context; + + /// <summary> + /// Load the main assembly for the plugin. + /// </summary> + public Assembly LoadDefaultAssembly() => _context.LoadAssemblyFromFilePath(_mainAssemblyPath); + + /// <summary> + /// Load an assembly by name. + /// </summary> + /// <param name="assemblyName">The assembly name.</param> + /// <returns>The assembly.</returns> + public Assembly LoadAssembly(AssemblyName assemblyName) => _context.LoadFromAssemblyName(assemblyName); + + /// <summary> + /// Sets the scope used by some System.Reflection APIs which might trigger assembly loading. + /// <para> + /// See https://github.com/dotnet/coreclr/blob/v3.0.0/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md for more details. + /// </para> + /// </summary> + /// <returns></returns> + public AssemblyLoadContext.ContextualReflectionScope EnterContextualReflection() + => _context.EnterContextualReflection(); + + /// <summary> + /// Unloads the internal assembly load context + /// </summary> + /// <param name="invokeGc">A value that indicates if a garbage collection should be run</param> + /// <exception cref="InvalidOperationException"></exception> + public void Destroy(bool invokeGc) + { + if (!IsUnloadable) + { + throw new InvalidOperationException("The current assembly context cannot be unloaded"); + } + + _context.Unload(); + + //Optionally wait for GC to finish + if (invokeGc) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + } + + + private static AssemblyLoadContextBuilder CreateLoadContextBuilder(PluginConfig config) + { + var builder = new AssemblyLoadContextBuilder(); + + builder.SetMainAssemblyPath(config.MainAssemblyPath); + builder.SetDefaultContext(config.DefaultContext); + + foreach (var ext in config.PrivateAssemblies) + { + builder.PreferLoadContextAssembly(ext); + } + + if (config.PreferSharedTypes) + { + builder.PreferDefaultLoadContext(true); + } + + if (config.IsUnloadable) + { + builder.EnableUnloading(); + } + + if (config.LoadInMemory) + { + builder.PreloadAssembliesIntoMemory(); + builder.ShadowCopyNativeLibraries(); + } + + foreach (var assemblyName in config.SharedAssemblies) + { + builder.PreferDefaultLoadContextAssembly(assemblyName); + } + + return builder; + } + } +} diff --git a/third-party/DotNetCorePlugins/src/Properties/AssemblyInfo.cs b/third-party/DotNetCorePlugins/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7ed81e4 --- /dev/null +++ b/third-party/DotNetCorePlugins/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("McMaster.NETCore.Plugins.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001001df0eba4297c8ffdf114a13714ad787744619dfb18e29191703f6f782d6a09e4a4cac35b8c768cbbd9ade8197bc0f66ec66fabc9071a206c8060af8b7a332236968d3ee44b90bd2f30d0edcb6150555c6f8d988e48234debaf2d427a08d7c06ba1343411142dc8ac996f7f7dbe0e93d13f17a7624db5400510e6144b0fd683b9")] |