Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Source/AssemblyResolving/ModInternalAssemblyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ private IEnumerable<string> EnumerateAssemblyFilesInMod()
foreach (var file in _mod.FileProxy.EnumerateFiles(""))
{
if (
file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
file.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)
(file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
file.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) &&
_mod.FileProxy.IsDotNetAssembly(file)
) {
yield return file;
}
Expand Down
28 changes: 27 additions & 1 deletion Source/FileProxies/DirectoryFileProxy.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace HatModLoader.Source.FileProxies
using System.Reflection;

namespace HatModLoader.Source.FileProxies
{
internal class DirectoryFileProxy : IFileProxy
{
Expand Down Expand Up @@ -37,6 +39,30 @@ public Stream OpenFile(string localPath)
return File.OpenRead(Path.Combine(modDirectory, localPath));
}

public IntPtr LoadLibrary(string localPath)
{
return NativeLibraryInterop.Load(Path.Combine(modDirectory, localPath));
}

public void UnloadLibrary(IntPtr handle)
{
NativeLibraryInterop.Free(handle);
}

public bool IsDotNetAssembly(string localPath)
{
try
{
var fullPath = Path.Combine(modDirectory, localPath);
AssemblyName.GetAssemblyName(fullPath);
return true;
}
catch (BadImageFormatException)
{
return false; // Native library file
}
}

public void Dispose() { }


Expand Down
3 changes: 3 additions & 0 deletions Source/FileProxies/IFileProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ public interface IFileProxy : IDisposable
public IEnumerable<string> EnumerateFiles(string localPath);
public bool FileExists(string localPath);
public Stream OpenFile(string localPath);
public IntPtr LoadLibrary(string localPath);
public void UnloadLibrary(IntPtr handle);
public bool IsDotNetAssembly(string localPath);
}
}
52 changes: 52 additions & 0 deletions Source/FileProxies/ZipFileProxy.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System.IO.Compression;
using System.Reflection;

namespace HatModLoader.Source.FileProxies
{
public class ZipFileProxy : IFileProxy
{
private ZipArchive archive;
private string zipPath;
private readonly Dictionary<IntPtr, string> tempFiles = [];
public string RootPath => zipPath;
public string ContainerName => Path.GetFileName(zipPath);

Expand Down Expand Up @@ -34,6 +36,56 @@ public Stream OpenFile(string localPath)
return archive.Entries.Where(e => e.FullName == localPath).First().Open();
}

private ZipArchiveEntry GetEntry(string localPath)
{
return archive.Entries.FirstOrDefault(e => e.FullName == localPath);
}

public IntPtr LoadLibrary(string localPath)
{
var tempFile = Path.GetTempFileName();
var entry = GetEntry(localPath);
entry.ExtractToFile(tempFile, true);

var handle = NativeLibraryInterop.Load(tempFile);
if (handle != IntPtr.Zero)
{
tempFiles.Add(handle, tempFile);
}

return handle;
}

public void UnloadLibrary(IntPtr handle)
{
if (tempFiles.TryGetValue(handle, out var tempFile))
{
NativeLibraryInterop.Free(handle);
File.Delete(tempFile);
tempFiles.Remove(handle);
}
}

public bool IsDotNetAssembly(string localPath)
{
var tempFile = Path.GetTempFileName();
var result = true;

try
{
var entry = GetEntry(localPath);
entry.ExtractToFile(tempFile, true);
AssemblyName.GetAssemblyName(tempFile);
}
catch (BadImageFormatException)
{
result = false; // Native library file
}

File.Delete(tempFile);
return result;
}

public void Dispose()
{
archive.Dispose();
Expand Down
43 changes: 42 additions & 1 deletion Source/ModDefinition/Metadata.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Xml.Serialization;
using System.Runtime.InteropServices;
using System.Xml.Serialization;
using Common;
using HatModLoader.Source.FileProxies;

Expand Down Expand Up @@ -34,6 +35,8 @@ public string VersionString

public DependencyInfo[] Dependencies { get; set; }

public NativeLibrary[] NativeDependencies { get; set; }

public static bool TryLoad(IFileProxy proxy, out Metadata metadata)
{
if (!proxy.FileExists(ModMetadataFile))
Expand Down Expand Up @@ -87,5 +90,43 @@ public string MinimumVersionString
}
}
}

[Serializable]
public struct NativeLibrary
{
[XmlAttribute] public Architecture Architecture { get; set; }

[XmlIgnore] public OSPlatform Platform { get; set; }

[XmlAttribute("Platform")]
public string PlatformString
{
get
{
if (Platform == OSPlatform.Windows) return "Windows";
if (Platform == OSPlatform.Linux) return "Linux";
return Platform == OSPlatform.OSX ? "OSX" : "Unknown";
}
set
{
Platform = value switch
{
"Windows" => OSPlatform.Windows,
"Linux" => OSPlatform.Linux,
"OSX" => OSPlatform.OSX,
_ => throw new ArgumentException($"Unknown platform: {value}")
};
}
}

[XmlText]
public string Path
{
get => _path;
set => _path = value?.Trim();
}

private string _path;
}
}
}
61 changes: 60 additions & 1 deletion Source/ModDefinition/ModContainer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using FezEngine.Tools;
using System.Runtime.InteropServices;
using Common;
using FezEngine.Tools;
using HatModLoader.Source.AssemblyResolving;
using HatModLoader.Source.Assets;
using HatModLoader.Source.FileProxies;
Expand All @@ -17,6 +19,8 @@ public class ModContainer : IDisposable
public CodeMod CodeMod { get; internal set; }

private IAssemblyResolver _assemblyResolver;

private readonly List<IntPtr> _nativeLibraryHandles = new();

public ModContainer(IFileProxy fileProxy, Metadata metadata)
{
Expand All @@ -30,6 +34,8 @@ public void Initialize(Game game)
{
_assemblyResolver = new ModInternalAssemblyResolver(this);
AssemblyResolverRegistry.Register(_assemblyResolver);
AppDomain.CurrentDomain.ProcessExit += UnloadNativeLibraries;
LoadNativeLibraries();
CodeMod?.Initialize(game);
}
}
Expand Down Expand Up @@ -59,4 +65,57 @@ public void Dispose()
AssemblyResolverRegistry.Unregister(_assemblyResolver);
}
}

private void UnloadNativeLibraries(object sender, EventArgs e)
{
lock (_nativeLibraryHandles)
{
foreach (var library in _nativeLibraryHandles)
{
FileProxy.UnloadLibrary(library);
}
}
}

private void LoadNativeLibraries()
{
if (Metadata.NativeDependencies == null || Metadata.NativeDependencies.Length == 0)
{
return;
}

var platformSpecific = Metadata.NativeDependencies
.Where(library => RuntimeInformation.IsOSPlatform(library.Platform))
.Where(library => RuntimeInformation.ProcessArchitecture == library.Architecture)
.ToArray();

if (platformSpecific.Length == 0)
{
var os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows"
: RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "Linux"
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "OSX"
: "Unknown";
var cpu = RuntimeInformation.ProcessArchitecture;
throw new PlatformNotSupportedException($"There're no native libraries found for " +
$"Platform=\"{os}\" Architecture=\"{cpu}\"");
}

foreach (var library in platformSpecific)
{
if (!FileProxy.FileExists(library.Path))
{
throw new DllNotFoundException($"There's no native library found at: {library.Path}");
}

var libraryHandle = FileProxy.LoadLibrary(library.Path);
if (libraryHandle == IntPtr.Zero)
{
Logger.Log(Metadata.Name, $"Unable to load native library: {library.Path}");
continue;
}

Logger.Log(Metadata.Name, $"Native library successfully loaded: {library.Path}");
_nativeLibraryHandles.Add(libraryHandle);
}
}
}
33 changes: 33 additions & 0 deletions Source/NativeLibraryInterop.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Runtime.InteropServices;

namespace HatModLoader.Source
{
public static class NativeLibraryInterop
{
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr LoadLibrary(string lpFileName);

[DllImport("libdl", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr Dlopen(string fileName, int flags);

[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool FreeLibrary(IntPtr hModule);

[DllImport("libdl", CallingConvention = CallingConvention.Cdecl)]
private static extern int Dlclose(IntPtr handle);

public static IntPtr Load(string fileName)
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? LoadLibrary(fileName)
: Dlopen(fileName, 1);
}

public static bool Free(IntPtr libraryHandle)
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? FreeLibrary(libraryHandle)
: Dlclose(libraryHandle) == 0;
}
}
}