using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Text; namespace ZeroLevel.Services.Windows { public sealed class WindowsLibraryLoader { #region Fields private const string ProcessorArchitecture = "PROCESSOR_ARCHITECTURE"; private const string DllFileExtension = ".dll"; private const string DllDirectory = "dll"; private readonly Dictionary _ProcessorArchitectureAddressWidthPlatforms = new Dictionary(StringComparer.OrdinalIgnoreCase) { {"x86", 4}, {"AMD64", 8}, {"IA64", 8}, {"ARM", 4} }; private readonly Dictionary _ProcessorArchitecturePlatforms = new Dictionary(StringComparer.OrdinalIgnoreCase) { {"x86", "x86"}, {"AMD64", "x64"}, {"IA64", "Itanium"}, {"ARM", "WinCE"} }; private readonly object _SyncLock = new object(); private static readonly IDictionary LoadedLibraries = new Dictionary(); [System.Runtime.InteropServices.DllImport("kernel32", EntryPoint = "LoadLibrary", CallingConvention = System.Runtime.InteropServices.CallingConvention.Winapi, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] private static extern IntPtr Win32LoadLibrary(string dllPath); #endregion #region Properties public static bool IsCurrentPlatformSupported() { return RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows); } public static bool IsWindows() { return Environment.OSVersion.Platform == PlatformID.Win32NT || Environment.OSVersion.Platform == PlatformID.Win32S || Environment.OSVersion.Platform == PlatformID.Win32Windows || Environment.OSVersion.Platform == PlatformID.WinCE; } #endregion #region Methods #region Helpers private static string FixUpDllFileName(string fileName) { if (string.IsNullOrEmpty(fileName)) return fileName; if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return fileName; if (!fileName.EndsWith(DllFileExtension, StringComparison.OrdinalIgnoreCase)) return $"{fileName}{DllFileExtension}"; return fileName; } private ProcessArchitectureInfo GetProcessArchitecture() { // BUG: Will this always be reliable? var processArchitecture = Environment.GetEnvironmentVariable(ProcessorArchitecture); var processInfo = new ProcessArchitectureInfo(); if (!string.IsNullOrEmpty(processArchitecture)) { // Sanity check processInfo.Architecture = processArchitecture; } else { processInfo.AddWarning("Failed to detect processor architecture, falling back to x86."); processInfo.Architecture = (IntPtr.Size == 8) ? "x64" : "x86"; } var addressWidth = this._ProcessorArchitectureAddressWidthPlatforms[processInfo.Architecture]; if (addressWidth != IntPtr.Size) { if (String.Equals(processInfo.Architecture, "AMD64", StringComparison.OrdinalIgnoreCase) && IntPtr.Size == 4) { // fall back to x86 if detected x64 but has an address width of 32 bits. processInfo.Architecture = "x86"; processInfo.AddWarning("Expected the detected processing architecture of {0} to have an address width of {1} Bytes but was {2} Bytes, falling back to x86.", processInfo.Architecture, addressWidth, IntPtr.Size); } else { // no fallback possible processInfo.AddWarning("Expected the detected processing architecture of {0} to have an address width of {1} Bytes but was {2} Bytes.", processInfo.Architecture, addressWidth, IntPtr.Size); } } return processInfo; } private string GetPlatformName(string processorArchitecture) { if (String.IsNullOrEmpty(processorArchitecture)) return null; string platformName; if (this._ProcessorArchitecturePlatforms.TryGetValue(processorArchitecture, out platformName)) { return platformName; } return null; } public void LoadLibraries(IEnumerable dlls) { if (!IsWindows()) return; foreach (var dll in dlls) LoadLibrary(dll); } private void LoadLibrary(string dllName) { if (!IsCurrentPlatformSupported()) return; try { lock (this._SyncLock) { if (LoadedLibraries.ContainsKey(dllName)) return; var processArch = GetProcessArchitecture(); IntPtr dllHandle; // Try loading from executing assembly domain var executingAssembly = GetType().GetTypeInfo().Assembly; var baseDirectory = Path.GetDirectoryName(executingAssembly.Location); dllHandle = LoadLibraryInternal(dllName, baseDirectory, processArch); if (dllHandle != IntPtr.Zero) return; // Gets the pathname of the base directory that the assembly resolver uses to probe for assemblies. // https://github.com/dotnet/corefx/issues/2221 baseDirectory = AppContext.BaseDirectory; dllHandle = LoadLibraryInternal(dllName, baseDirectory, processArch); if (dllHandle != IntPtr.Zero) return; // Finally try the working directory baseDirectory = Path.GetFullPath(Directory.GetCurrentDirectory()); dllHandle = LoadLibraryInternal(dllName, baseDirectory, processArch); if (dllHandle != IntPtr.Zero) return; var errorMessage = new StringBuilder(); errorMessage.Append($"Failed to find dll \"{dllName}\", for processor architecture {processArch.Architecture}."); if (processArch.HasWarnings) { // include process detection warnings errorMessage.Append($"\r\nWarnings: \r\n{processArch.WarningText()}"); } throw new Exception(errorMessage.ToString()); } } catch (Exception e) { System.Diagnostics.Debug.WriteLine(e.Message); } } private IntPtr LoadLibraryInternal(string dllName, string baseDirectory, ProcessArchitectureInfo processArchInfo) { var platformName = GetPlatformName(processArchInfo.Architecture); var expectedDllDirectory = Path.Combine( Path.Combine(baseDirectory, DllDirectory), platformName); return this.LoadLibraryRaw(dllName, expectedDllDirectory); } private IntPtr LoadLibraryRaw(string dllName, string baseDirectory) { var libraryHandle = IntPtr.Zero; var fileName = FixUpDllFileName(Path.Combine(baseDirectory, dllName)); // Show where we're trying to load the file from Debug.WriteLine($"Trying to load native library \"{fileName}\"..."); if (File.Exists(fileName)) { // Attempt to load dll try { libraryHandle = Win32LoadLibrary(fileName); if (libraryHandle != IntPtr.Zero) { // library has been loaded Debug.WriteLine($"Successfully loaded native library \"{fileName}\"."); LoadedLibraries.Add(dllName, libraryHandle); } else { Debug.WriteLine($"Failed to load native library \"{fileName}\".\r\nCheck windows event log."); } } catch (Exception e) { var lastError = Marshal.GetLastWin32Error(); Debug.WriteLine($"Failed to load native library \"{fileName}\".\r\nLast Error:{lastError}\r\nCheck inner exception and\\or windows event log.\r\nInner Exception: {e}"); } } else { Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, "The native library \"{0}\" does not exist.", fileName)); } return libraryHandle; } #endregion #endregion private class ProcessArchitectureInfo { #region Constructors public ProcessArchitectureInfo() { this.Warnings = new List(); } #endregion #region Properties public string Architecture { get; set; } private List Warnings { get; } #endregion #region Methods public void AddWarning(string format, params object[] args) { Warnings.Add(String.Format(format, args)); } public bool HasWarnings => Warnings.Count > 0; public string WarningText() { return string.Join("\r\n", Warnings.ToArray()); } #endregion } } }