- In MixFile, the Distinct call doesn't presize the HashSet it uses internally. As we know we will enumerate all results, create the HashSet ourselves so that is it presized correctly. - In ObjectCreator, stream the assembly when hashing rather than reading all bytes into memory. These changes avoid some allocations on the large object heap, in turn this means the GC can avoid performing unnecessary Gen 2 collections just to clear down the LOH.
210 lines
6.4 KiB
C#
210 lines
6.4 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using OpenRA.Primitives;
|
|
|
|
namespace OpenRA
|
|
{
|
|
public sealed class ObjectCreator : IDisposable
|
|
{
|
|
// .NET does not support unloading assemblies, so mod libraries will leak across mod changes.
|
|
// This tracks the assemblies that have been loaded since game start so that we don't load multiple copies
|
|
static readonly Dictionary<string, Assembly> ResolvedAssemblies = new();
|
|
|
|
readonly Cache<string, Type> typeCache;
|
|
readonly Cache<Type, ConstructorInfo> ctorCache;
|
|
readonly (Assembly Assembly, string Namespace)[] assemblies;
|
|
|
|
public ObjectCreator(Manifest manifest, InstalledMods mods)
|
|
{
|
|
typeCache = new Cache<string, Type>(FindType);
|
|
ctorCache = new Cache<Type, ConstructorInfo>(GetCtor);
|
|
|
|
// Allow mods to load types from the core Game assembly, and any additional assemblies they specify.
|
|
// Assemblies can only be loaded from directories to avoid circular dependencies on package loaders.
|
|
var assemblyList = new List<Assembly>() { typeof(Game).Assembly };
|
|
foreach (var path in manifest.Assemblies)
|
|
{
|
|
var resolvedPath = FileSystem.FileSystem.ResolveAssemblyPath(path, manifest, mods);
|
|
if (resolvedPath == null)
|
|
throw new FileNotFoundException($"Assembly `{path}` not found.");
|
|
|
|
LoadAssembly(assemblyList, resolvedPath);
|
|
}
|
|
|
|
AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
|
|
assemblies = assemblyList.SelectMany(asm => asm.GetNamespaces().Select(ns => (asm, ns))).ToArray();
|
|
}
|
|
|
|
static void LoadAssembly(List<Assembly> assemblyList, string resolvedPath)
|
|
{
|
|
// .NET doesn't provide any way of querying the metadata of an assembly without either:
|
|
// (a) loading duplicate data into the application domain, breaking the world.
|
|
// (b) crashing if the assembly has already been loaded.
|
|
// We can't check the internal name of the assembly, so we'll work off the data instead
|
|
string hash;
|
|
using (var stream = File.OpenRead(resolvedPath))
|
|
hash = CryptoUtil.SHA1Hash(stream);
|
|
|
|
if (!ResolvedAssemblies.TryGetValue(hash, out var assembly))
|
|
{
|
|
#if NET5_0_OR_GREATER
|
|
var loader = new Support.AssemblyLoader(resolvedPath);
|
|
assembly = loader.LoadDefaultAssembly();
|
|
ResolvedAssemblies.Add(hash, assembly);
|
|
#else
|
|
assembly = Assembly.LoadFile(resolvedPath);
|
|
ResolvedAssemblies.Add(hash, assembly);
|
|
|
|
// Allow mods to use libraries.
|
|
var assemblyPath = Path.GetDirectoryName(resolvedPath);
|
|
if (assemblyPath != null)
|
|
{
|
|
foreach (var referencedAssembly in assembly.GetReferencedAssemblies())
|
|
{
|
|
var depedencyPath = Path.Combine(assemblyPath, referencedAssembly.Name + ".dll");
|
|
if (File.Exists(depedencyPath))
|
|
LoadAssembly(assemblyList, depedencyPath);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
assemblyList.Add(assembly);
|
|
}
|
|
|
|
Assembly ResolveAssembly(object sender, ResolveEventArgs e)
|
|
{
|
|
foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
|
|
if (a.FullName == e.Name)
|
|
return a;
|
|
|
|
return assemblies?.Select(a => a.Assembly).FirstOrDefault(a => a.FullName == e.Name);
|
|
}
|
|
|
|
// Only used by the linter to prevent exceptions from being thrown during a lint run
|
|
public static Action<string> MissingTypeAction = null;
|
|
|
|
public T CreateObject<T>(string className)
|
|
{
|
|
return CreateObject<T>(className, new Dictionary<string, object>());
|
|
}
|
|
|
|
public T CreateObject<T>(string className, Dictionary<string, object> args)
|
|
{
|
|
var type = typeCache[className];
|
|
if (type == null)
|
|
{
|
|
// HACK: The linter does not want to crash but only print an error instead
|
|
if (MissingTypeAction != null)
|
|
MissingTypeAction(className);
|
|
else
|
|
throw new InvalidOperationException($"Cannot locate type: {className}");
|
|
|
|
return default;
|
|
}
|
|
|
|
var ctor = ctorCache[type];
|
|
if (ctor == null)
|
|
return (T)CreateBasic(type);
|
|
else
|
|
return (T)CreateUsingArgs(ctor, args);
|
|
}
|
|
|
|
public Type FindType(string className)
|
|
{
|
|
return assemblies
|
|
.Select(pair => pair.Assembly.GetType(pair.Namespace + "." + className, false))
|
|
.FirstOrDefault(t => t != null);
|
|
}
|
|
|
|
public ConstructorInfo GetCtor(Type type)
|
|
{
|
|
var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
|
|
var ctors = type.GetConstructors(flags).Where(x => x.HasAttribute<UseCtorAttribute>());
|
|
if (ctors.Count() > 1)
|
|
throw new InvalidOperationException("ObjectCreator: UseCtor on multiple constructors; invalid.");
|
|
return ctors.FirstOrDefault();
|
|
}
|
|
|
|
public object CreateBasic(Type type)
|
|
{
|
|
return type.GetConstructor(Array.Empty<Type>()).Invoke(Array.Empty<object>());
|
|
}
|
|
|
|
public object CreateUsingArgs(ConstructorInfo ctor, Dictionary<string, object> args)
|
|
{
|
|
var p = ctor.GetParameters();
|
|
var a = new object[p.Length];
|
|
for (var i = 0; i < p.Length; i++)
|
|
{
|
|
var key = p[i].Name;
|
|
if (!args.ContainsKey(key)) throw new InvalidOperationException($"ObjectCreator: key `{key}' not found");
|
|
a[i] = args[key];
|
|
}
|
|
|
|
return ctor.Invoke(a);
|
|
}
|
|
|
|
public IEnumerable<Type> GetTypesImplementing<T>()
|
|
{
|
|
var it = typeof(T);
|
|
return GetTypes().Where(t => t != it && it.IsAssignableFrom(t));
|
|
}
|
|
|
|
public IEnumerable<Type> GetTypes()
|
|
{
|
|
return assemblies.Select(ma => ma.Assembly).Distinct()
|
|
.SelectMany(ma => ma.GetTypes());
|
|
}
|
|
|
|
public TLoader[] GetLoaders<TLoader>(IEnumerable<string> formats, string name)
|
|
{
|
|
var loaders = new List<TLoader>();
|
|
foreach (var format in formats)
|
|
{
|
|
var loader = FindType(format + "Loader");
|
|
if (loader == null || !loader.GetInterfaces().Contains(typeof(TLoader)))
|
|
throw new InvalidOperationException($"Unable to find a {name} loader for type '{format}'.");
|
|
|
|
loaders.Add((TLoader)CreateBasic(loader));
|
|
}
|
|
|
|
return loaders.ToArray();
|
|
}
|
|
|
|
~ObjectCreator()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
AppDomain.CurrentDomain.AssemblyResolve -= ResolveAssembly;
|
|
}
|
|
|
|
[AttributeUsage(AttributeTargets.Constructor)]
|
|
public sealed class UseCtorAttribute : Attribute { }
|
|
}
|
|
}
|