fix dependency loading, import plugin library

This commit is contained in:
Raymond 2021-09-27 16:43:50 -04:00
parent 6ba5525812
commit 8793456a0b
10 changed files with 1328 additions and 20 deletions

View file

@ -51,8 +51,11 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Warnings"> <PropertyGroup Label="Warnings">
<NoWarn>IDE0002;IDE0003;IDE0044;IDE1006;CA1822;CS1591;CS1701;CS1702</NoWarn> <NoWarn>IDE0002;IDE0003;IDE1006;IDE0044;CA1822;CS1591;CS1701;CS1702</NoWarn>
<!-- IDE1002 - Simplify member access -->
<!-- IDE1003 - This and me preferences -->
<!-- IDE1006 - Naming violation --> <!-- IDE1006 - Naming violation -->
<!-- IDE1006 - Add readonly modifier -->
<!-- CA1822 - Can be marked as static --> <!-- CA1822 - Can be marked as static -->
<!-- CS1591 - Missing XML comment for publicly visible type or member --> <!-- CS1591 - Missing XML comment for publicly visible type or member -->
<!-- CS1701 - Runtime policy may be needed --> <!-- CS1701 - Runtime policy may be needed -->
@ -65,9 +68,8 @@
<PackageReference Include="Lib.Harmony" Version="2.1.1" /> <PackageReference Include="Lib.Harmony" Version="2.1.1" />
<PackageReference Include="Lumina" Version="3.3.0" /> <PackageReference Include="Lumina" Version="3.3.0" />
<PackageReference Include="Lumina.Excel" Version="5.50.0" /> <PackageReference Include="Lumina.Excel" Version="5.50.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.10.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.10.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog" Version="2.10.0" /> <PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />

View file

@ -0,0 +1,316 @@
// Copyright (c) Nate McMaster, Dalamud contributors.
// Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Dalamud.Plugin.Internal.Loader.LibraryModel;
namespace Dalamud.Plugin.Internal.Loader
{
/// <summary>
/// A builder for creating an instance of <see cref="AssemblyLoadContext" />.
/// </summary>
internal 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>(this.resourceProbingPaths);
foreach (var additionalPath in this.additionalProbingPaths)
{
foreach (var subPath in this.resourceProbingSubpaths)
{
resourceProbingPaths.Add(Path.Combine(additionalPath, subPath));
}
}
if (this.mainAssemblyPath == null)
throw new InvalidOperationException($"Missing required property. You must call '{nameof(this.SetMainAssemblyPath)}' to configure the default assembly.");
return new ManagedLoadContext(
this.mainAssemblyPath,
this.managedLibraries,
this.nativeLibraries,
this.privateAssemblies,
this.defaultAssemblies,
this.additionalProbingPaths,
resourceProbingPaths,
this.defaultLoadContext,
this.preferDefaultLoadContext,
this.isCollectible,
this.loadInMemory,
this.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));
this.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)
{
this.defaultLoadContext = context ?? throw new ArgumentException($"Bad Argument: AssemblyLoadContext in {nameof(AssemblyLoadContextBuilder)}.{nameof(this.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)
this.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>(new[] { assemblyName });
while (names.TryDequeue(out var name))
{
if (name.Name == null || this.defaultAssemblies.Contains(name.Name))
{
// base cases
continue;
}
this.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 = this.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)
{
this.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)
{
this.managedLibraries.Add(library.Name.Name, library);
}
return this;
}
/// <summary>
/// Add a native library to the load context.
/// </summary>
/// <param name="library">A native library.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddNativeLibrary(NativeLibrary library)
{
ValidateRelativePath(library.AppLocalPath);
ValidateRelativePath(library.AdditionalProbingPath);
this.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));
this.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));
this.resourceProbingPaths.Add(path);
return this;
}
/// <summary>
/// Enable unloading the assembly load context.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder EnableUnloading()
{
this.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()
{
this.loadInMemory = true;
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()
{
this.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));
this.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));
}
}
}

View file

@ -0,0 +1,178 @@
https://github.com/natemcmaster/DotNetCorePlugins
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -0,0 +1,72 @@
// Copyright (c) Nate McMaster, Dalamud contributors.
// Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information.
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
namespace Dalamud.Plugin.Internal.Loader.LibraryModel
{
/// <summary>
/// Represents a managed, .NET assembly.
/// </summary>
[DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
internal class ManagedLibrary
{
private ManagedLibrary(AssemblyName name, string additionalProbingPath, string appLocalPath)
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
this.AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
}
/// <summary>
/// Gets the name of the managed library.
/// </summary>
public AssemblyName Name { get; }
/// <summary>
/// Gets the 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; }
/// <summary>
/// Gets the 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; }
/// <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>A managed library.</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);
}
}
}

View file

@ -0,0 +1,68 @@
// Copyright (c) Nate McMaster, Dalamud contributors.
// Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information.
using System;
using System.Diagnostics;
using System.IO;
namespace Dalamud.Plugin.Internal.Loader.LibraryModel
{
/// <summary>
/// Represents an unmanaged library, such as `libsqlite3`, which may need to be loaded
/// for P/Invoke to work.
/// </summary>
[DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
internal class NativeLibrary
{
private NativeLibrary(string name, string appLocalPath, string additionalProbingPath)
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
this.AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
}
/// <summary>
/// Gets the 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; }
/// <summary>
/// Gets the 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; }
/// <summary>
/// Gets the 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; }
/// <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>A native library.</returns>
public static NativeLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
{
return new NativeLibrary(
Path.GetFileNameWithoutExtension(assetPath),
assetPath,
Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath));
}
}
}

View file

@ -0,0 +1,77 @@
// Copyright (c) Nate McMaster, Dalamud contributors.
// Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
namespace Dalamud.Plugin.Internal.Loader
{
/// <summary>
/// Represents the configuration for a plugin loader.
/// </summary>
internal class LoaderConfig
{
/// <summary>
/// Initializes a new instance of the <see cref="LoaderConfig"/> class.
/// </summary>
/// <param name="mainAssemblyPath">The full file path to the main assembly for the plugin.</param>
public LoaderConfig(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));
if (!File.Exists(mainAssemblyPath))
throw new ArgumentException("Value must exist", nameof(mainAssemblyPath));
this.MainAssemblyPath = mainAssemblyPath;
}
/// <summary>
/// Gets the file path to the main assembly.
/// </summary>
public string MainAssemblyPath { get; }
/// <summary>
/// Gets a list of assemblies which should be treated as private.
/// </summary>
public ICollection<AssemblyName> PrivateAssemblies { get; } = new List<AssemblyName>();
/// <summary>
/// Gets 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">what-are-shared-types</seealso>
public ICollection<AssemblyName> SharedAssemblies { get; } = new List<AssemblyName>();
/// <summary>
/// Gets or sets a value indicating whether 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">what-are-shared-types</seealso>
/// </summary>
public bool PreferSharedTypes { get; set; }
/// <summary>
/// Gets or sets 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>
/// Gets or sets a value indicating whether the plugin can be unloaded from memory.
/// </summary>
public bool IsUnloadable { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to load assemblies into memory in order to not lock files.
/// </summary>
public bool LoadInMemory { get; set; }
}
}

View file

@ -0,0 +1,397 @@
// Copyright (c) Nate McMaster, Dalamud contributors.
// Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Dalamud.Plugin.Internal.Loader.LibraryModel;
namespace Dalamud.Plugin.Internal.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;
/// <summary>
/// Initializes a new instance of the <see cref="ManagedLoadContext"/> class.
/// </summary>
/// <param name="mainAssemblyPath">Main assembly path.</param>
/// <param name="managedAssemblies">Managed assemblies.</param>
/// <param name="nativeLibraries">Native assemblies.</param>
/// <param name="privateAssemblies">Private assemblies.</param>
/// <param name="defaultAssemblies">Default assemblies.</param>
/// <param name="additionalProbingPaths">Additional probing paths.</param>
/// <param name="resourceProbingPaths">Resource probing paths.</param>
/// <param name="defaultLoadContext">Default load context.</param>
/// <param name="preferDefaultLoadContext">If the default load context should be prefered.</param>
/// <param name="isCollectible">If the dll is collectible.</param>
/// <param name="loadInMemory">If the dll should be loaded in memory.</param>
/// <param name="shadowCopyNativeLibraries">If native libraries should be shadow copied.</param>
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)
{
if (resourceProbingPaths == null)
throw new ArgumentNullException(nameof(resourceProbingPaths));
this.mainAssemblyPath = mainAssemblyPath ?? throw new ArgumentNullException(nameof(mainAssemblyPath));
this.dependencyResolver = new AssemblyDependencyResolver(mainAssemblyPath);
this.basePath = Path.GetDirectoryName(mainAssemblyPath) ?? throw new ArgumentException("Invalid assembly path", nameof(mainAssemblyPath));
this.managedAssemblies = managedAssemblies ?? throw new ArgumentNullException(nameof(managedAssemblies));
this.privateAssemblies = privateAssemblies ?? throw new ArgumentNullException(nameof(privateAssemblies));
this.defaultAssemblies = defaultAssemblies != null ? defaultAssemblies.ToList() : throw new ArgumentNullException(nameof(defaultAssemblies));
this.nativeLibraries = nativeLibraries ?? throw new ArgumentNullException(nameof(nativeLibraries));
this.additionalProbingPaths = additionalProbingPaths ?? throw new ArgumentNullException(nameof(additionalProbingPaths));
this.defaultLoadContext = defaultLoadContext;
this.preferDefaultLoadContext = preferDefaultLoadContext;
this.loadInMemory = loadInMemory;
this.resourceRoots = new[] { this.basePath }
.Concat(resourceProbingPaths)
.ToArray();
this.shadowCopyNativeLibraries = shadowCopyNativeLibraries;
this.unmanagedDllShadowCopyDirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
if (shadowCopyNativeLibraries)
{
this.Unloading += _ => this.OnUnloaded();
}
}
/// <summary>
/// Load an assembly from a filepath.
/// </summary>
/// <param name="path">Assembly path.</param>
/// <returns>A loaded assembly.</returns>
public Assembly LoadAssemblyFromFilePath(string path)
{
if (!this.loadInMemory)
return this.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 this.LoadFromStream(file, pdbFile);
}
return this.LoadFromStream(file);
}
/// <summary>
/// Load an assembly.
/// </summary>
/// <param name="assemblyName">Name of the assembly.</param>
/// <returns>Loaded assembly.</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 ((this.preferDefaultLoadContext || this.defaultAssemblies.Contains(assemblyName.Name)) && !this.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 = this.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 = this.dependencyResolver.ResolveAssemblyToPath(assemblyName);
if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
{
return this.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(assemblyName.CultureName, "neutral"))
{
foreach (var resourceRoot in this.resourceRoots)
{
var resourcePath = Path.Combine(resourceRoot, assemblyName.CultureName, assemblyName.Name + ".dll");
if (File.Exists(resourcePath))
{
return this.LoadAssemblyFromFilePath(resourcePath);
}
}
return null;
}
if (this.managedAssemblies.TryGetValue(assemblyName.Name, out var library) && library != null)
{
if (this.SearchForLibrary(library, out var path) && path != null)
{
return this.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 this.additionalProbingPaths.Prepend(this.basePath))
{
var localFile = Path.Combine(probingPath, dllName);
if (File.Exists(localFile))
{
return this.LoadAssemblyFromFilePath(localFile);
}
}
}
return null;
}
/// <summary>
/// Loads the unmanaged binary using configured list of native libraries.
/// </summary>
/// <param name="unmanagedDllName">Unmanaged DLL name.</param>
/// <returns>The unmanaged dll handle.</returns>
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var resolvedPath = this.dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
{
return this.LoadUnmanagedDllFromResolvedPath(resolvedPath, normalizePath: false);
}
foreach (var prefix in PlatformInformation.NativeLibraryPrefixes)
{
if (this.nativeLibraries.TryGetValue(prefix + unmanagedDllName, out var library))
{
if (this.SearchForLibrary(library, prefix, out var path) && path != null)
{
return this.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 (this.nativeLibraries.TryGetValue(prefix + trimmedName, out library))
{
if (this.SearchForLibrary(library, prefix, out var path) && path != null)
{
return this.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 this.additionalProbingPaths.Prepend(this.basePath))
{
var localFile = Path.Combine(probingPath, prefixSuffixDllName);
if (File.Exists(localFile))
{
return this.LoadUnmanagedDllFromResolvedPath(localFile);
}
var localFileWithoutSuffix = Path.Combine(probingPath, prefixDllName);
if (File.Exists(localFileWithoutSuffix))
{
return this.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(this.basePath, library.AppLocalPath);
if (File.Exists(localFile))
{
path = localFile;
return true;
}
// 2. Search additional probing paths
foreach (var searchPath in this.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(this.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(this.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(this.basePath, library.AppLocalPath);
if (File.Exists(local))
{
path = local;
return true;
}
// 3. Search additional probing paths
foreach (var searchPath in this.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 this.shadowCopyNativeLibraries
? this.LoadUnmanagedDllFromShadowCopy(unmanagedDllPath)
: this.LoadUnmanagedDllFromPath(unmanagedDllPath);
}
private IntPtr LoadUnmanagedDllFromShadowCopy(string unmanagedDllPath)
{
var shadowCopyDllPath = this.CreateShadowCopy(unmanagedDllPath);
return this.LoadUnmanagedDllFromPath(shadowCopyDllPath);
}
private string CreateShadowCopy(string dllPath)
{
Directory.CreateDirectory(this.unmanagedDllShadowCopyDirectoryPath);
var dllFileName = Path.GetFileName(dllPath);
var shadowCopyPath = Path.Combine(this.unmanagedDllShadowCopyDirectoryPath, dllFileName);
if (!File.Exists(shadowCopyPath))
{
File.Copy(dllPath, shadowCopyPath);
}
return shadowCopyPath;
}
private void OnUnloaded()
{
if (!this.shadowCopyNativeLibraries || !Directory.Exists(this.unmanagedDllShadowCopyDirectoryPath))
{
return;
}
// Attempt to delete shadow copies
try
{
Directory.Delete(this.unmanagedDllShadowCopyDirectoryPath, recursive: true);
}
catch (Exception)
{
// Files might be locked by host process. Nothing we can do about it, I guess.
}
}
}
}

View file

@ -0,0 +1,32 @@
// Copyright (c) Nate McMaster, Dalamud team.
// Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information.
namespace Dalamud.Plugin.Internal.Loader
{
/// <summary>
/// Platform specific information.
/// </summary>
internal class PlatformInformation
{
/// <summary>
/// Gets a list of native OS specific library extensions.
/// </summary>
public static string[] NativeLibraryExtensions => new[] { ".dll" };
/// <summary>
/// Gets a list of native OS specific library prefixes.
/// </summary>
public static string[] NativeLibraryPrefixes => new[] { string.Empty };
/// <summary>
/// Gets a list of native OS specific managed assembly extensions.
/// </summary>
public static string[] ManagedAssemblyExtensions => new[]
{
".dll",
".ni.dll",
".exe",
".ni.exe",
};
}
}

View file

@ -0,0 +1,163 @@
// Copyright (c) Nate McMaster, Dalamud team.
// Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information.
using System;
using System.Reflection;
using System.Runtime.Loader;
namespace Dalamud.Plugin.Internal.Loader
{
/// <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>
internal class PluginLoader : IDisposable
{
private readonly LoaderConfig config;
private readonly AssemblyLoadContextBuilder contextBuilder;
private ManagedLoadContext context;
private volatile bool disposed;
/// <summary>
/// Initializes a new instance of the <see cref="PluginLoader"/> class.
/// </summary>
/// <param name="config">The configuration for the plugin.</param>
public PluginLoader(LoaderConfig config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
this.contextBuilder = CreateLoadContextBuilder(config);
this.context = (ManagedLoadContext)this.contextBuilder.Build();
}
/// <summary>
/// Gets a value indicating whether this plugin is capable of being unloaded.
/// </summary>
public bool IsUnloadable
=> this.context.IsCollectible;
/// <summary>
/// Gets the assembly load context.
/// </summary>
public AssemblyLoadContext LoadContext => this.context;
/// <summary>
/// Create a plugin loader for an assembly file.
/// </summary>
/// <param name="assemblyFile">The file path to the main assembly for the plugin.</param>
/// <param name="configure">A function which can be used to configure advanced options for the plugin loader.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Action<LoaderConfig> configure)
{
if (configure == null)
throw new ArgumentNullException(nameof(configure));
var config = new LoaderConfig(assemblyFile);
configure(config);
return new PluginLoader(config);
}
/// <summary>
/// The unloads and reloads the plugin assemblies.
/// This method throws if <see cref="IsUnloadable" /> is <c>false</c>.
/// </summary>
public void Reload()
{
this.EnsureNotDisposed();
if (!this.IsUnloadable)
{
throw new InvalidOperationException("Reload cannot be used because IsUnloadable is false");
}
this.context.Unload();
this.context = (ManagedLoadContext)this.contextBuilder.Build();
GC.Collect();
GC.WaitForPendingFinalizers();
}
/// <summary>
/// Load the main assembly for the plugin.
/// </summary>
/// <returns>The assembly.</returns>
public Assembly LoadDefaultAssembly()
{
this.EnsureNotDisposed();
return this.context.LoadAssemblyFromFilePath(this.config.MainAssemblyPath);
}
/// <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>A contextual reflection scope.</returns>
public AssemblyLoadContext.ContextualReflectionScope EnterContextualReflection()
=> this.context.EnterContextualReflection();
/// <summary>
/// Disposes the plugin loader. This only does something if <see cref="IsUnloadable" /> is true.
/// When true, this will unload assemblies which which were loaded during the lifetime
/// of the plugin.
/// </summary>
public void Dispose()
{
if (this.disposed)
return;
this.disposed = true;
if (this.context.IsCollectible)
this.context.Unload();
}
private static AssemblyLoadContextBuilder CreateLoadContextBuilder(LoaderConfig 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;
}
private void EnsureNotDisposed()
{
if (this.disposed)
throw new ObjectDisposedException(nameof(PluginLoader));
}
}
}

View file

@ -8,8 +8,8 @@ using Dalamud.Game;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Loader;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
using McMaster.NETCore.Plugins;
namespace Dalamud.Plugin.Internal namespace Dalamud.Plugin.Internal
{ {
@ -37,17 +37,18 @@ namespace Dalamud.Plugin.Internal
/// <param name="manifest">The plugin manifest.</param> /// <param name="manifest">The plugin manifest.</param>
public LocalPlugin(FileInfo dllFile, LocalPluginManifest? manifest) public LocalPlugin(FileInfo dllFile, LocalPluginManifest? manifest)
{ {
if (dllFile.Name == "FFXIVClientStructs.Generators.dll")
{
// Could this be done another way? Sure. It is an extremely common source
// of errors in the log through, and should never be loaded as a plugin.
Log.Error($"Not a plugin: {dllFile.FullName}");
throw new InvalidPluginException(dllFile);
}
this.DllFile = dllFile; this.DllFile = dllFile;
this.State = PluginState.Unloaded; this.State = PluginState.Unloaded;
this.loader = PluginLoader.CreateFromAssemblyFile( this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, this.SetupLoaderConfig);
this.DllFile.FullName,
config =>
{
config.IsUnloadable = true;
config.LoadInMemory = true;
config.PreferSharedTypes = true;
});
try try
{ {
@ -252,14 +253,7 @@ namespace Dalamud.Plugin.Internal
try try
{ {
this.loader ??= PluginLoader.CreateFromAssemblyFile( this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, this.SetupLoaderConfig);
this.DllFile.FullName,
config =>
{
config.IsUnloadable = true;
config.LoadInMemory = true;
config.PreferSharedTypes = true;
});
if (reloading) if (reloading)
{ {
@ -437,6 +431,15 @@ namespace Dalamud.Plugin.Internal
this.SaveManifest(); this.SaveManifest();
} }
private void SetupLoaderConfig(LoaderConfig config)
{
config.IsUnloadable = true;
config.LoadInMemory = true;
config.PreferSharedTypes = false;
config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName());
config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName());
}
private void SaveManifest() => this.Manifest.Save(this.manifestFile); private void SaveManifest() => this.Manifest.Save(this.manifestFile);
} }
} }