diff --git a/Source/AssemblyResolving/ModInternalAssemblyResolver.cs b/Source/AssemblyResolving/ModInternalAssemblyResolver.cs index 5a44c2b..3154d40 100644 --- a/Source/AssemblyResolving/ModInternalAssemblyResolver.cs +++ b/Source/AssemblyResolving/ModInternalAssemblyResolver.cs @@ -56,8 +56,9 @@ private IEnumerable 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; } diff --git a/Source/FileProxies/DirectoryFileProxy.cs b/Source/FileProxies/DirectoryFileProxy.cs index d46f7ea..db00d7f 100644 --- a/Source/FileProxies/DirectoryFileProxy.cs +++ b/Source/FileProxies/DirectoryFileProxy.cs @@ -1,4 +1,6 @@ -namespace HatModLoader.Source.FileProxies +using System.Reflection; + +namespace HatModLoader.Source.FileProxies { internal class DirectoryFileProxy : IFileProxy { @@ -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() { } diff --git a/Source/FileProxies/IFileProxy.cs b/Source/FileProxies/IFileProxy.cs index 2ee1e7b..de7ab80 100644 --- a/Source/FileProxies/IFileProxy.cs +++ b/Source/FileProxies/IFileProxy.cs @@ -7,5 +7,8 @@ public interface IFileProxy : IDisposable public IEnumerable 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); } } diff --git a/Source/FileProxies/ZipFileProxy.cs b/Source/FileProxies/ZipFileProxy.cs index 6d69414..c4216a8 100644 --- a/Source/FileProxies/ZipFileProxy.cs +++ b/Source/FileProxies/ZipFileProxy.cs @@ -1,4 +1,5 @@ using System.IO.Compression; +using System.Reflection; namespace HatModLoader.Source.FileProxies { @@ -6,6 +7,7 @@ public class ZipFileProxy : IFileProxy { private ZipArchive archive; private string zipPath; + private readonly Dictionary tempFiles = []; public string RootPath => zipPath; public string ContainerName => Path.GetFileName(zipPath); @@ -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(); diff --git a/Source/ModDefinition/Metadata.cs b/Source/ModDefinition/Metadata.cs index 01e8c78..26d5cc1 100644 --- a/Source/ModDefinition/Metadata.cs +++ b/Source/ModDefinition/Metadata.cs @@ -1,4 +1,5 @@ -using System.Xml.Serialization; +using System.Runtime.InteropServices; +using System.Xml.Serialization; using Common; using HatModLoader.Source.FileProxies; @@ -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)) @@ -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; + } } } \ No newline at end of file diff --git a/Source/ModDefinition/ModContainer.cs b/Source/ModDefinition/ModContainer.cs index b4ca05f..e506bdd 100644 --- a/Source/ModDefinition/ModContainer.cs +++ b/Source/ModDefinition/ModContainer.cs @@ -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; @@ -17,6 +19,8 @@ public class ModContainer : IDisposable public CodeMod CodeMod { get; internal set; } private IAssemblyResolver _assemblyResolver; + + private readonly List _nativeLibraryHandles = new(); public ModContainer(IFileProxy fileProxy, Metadata metadata) { @@ -30,6 +34,8 @@ public void Initialize(Game game) { _assemblyResolver = new ModInternalAssemblyResolver(this); AssemblyResolverRegistry.Register(_assemblyResolver); + AppDomain.CurrentDomain.ProcessExit += UnloadNativeLibraries; + LoadNativeLibraries(); CodeMod?.Initialize(game); } } @@ -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); + } + } } \ No newline at end of file diff --git a/Source/NativeLibraryInterop.cs b/Source/NativeLibraryInterop.cs new file mode 100644 index 0000000..c8c1ccf --- /dev/null +++ b/Source/NativeLibraryInterop.cs @@ -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; + } + } +} \ No newline at end of file