From 5df5ac8fabb666b9f5ccc1237417ca98afc4f3fd Mon Sep 17 00:00:00 2001 From: Ilya Date: Tue, 10 Dec 2019 01:03:53 +0500 Subject: [PATCH] Add native binary resolver (#11032) --- .../CoreCLR/CorePsAssemblyLoadContext.cs | 95 ++++++++++++++++--- .../Basic/Assembly.LoadNative.Tests.ps1 | 72 ++++++++++++++ 2 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 test/powershell/engine/Basic/Assembly.LoadNative.Tests.ps1 diff --git a/src/System.Management.Automation/CoreCLR/CorePsAssemblyLoadContext.cs b/src/System.Management.Automation/CoreCLR/CorePsAssemblyLoadContext.cs index 3d55ed1b67..bd83357807 100644 --- a/src/System.Management.Automation/CoreCLR/CorePsAssemblyLoadContext.cs +++ b/src/System.Management.Automation/CoreCLR/CorePsAssemblyLoadContext.cs @@ -1,19 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#if CORECLR - -using System.IO; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; +using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Reflection; -using System.Reflection.Metadata; -using System.Reflection.PortableExecutable; using System.Runtime.Loader; -using System.Text; -using System.Linq; namespace System.Management.Automation { @@ -46,7 +41,9 @@ namespace System.Management.Automation lock (s_syncObj) { if (Instance != null) + { throw new InvalidOperationException(SingletonAlreadyInitialized); + } Instance = new PowerShellAssemblyLoadContext(basePaths); return Instance; @@ -93,8 +90,15 @@ namespace System.Management.Automation _availableDotNetAssemblyNames = new Lazy>( () => new HashSet(_coreClrTypeCatalog.Values, StringComparer.Ordinal)); - // LAST: Register 'Resolving' handler on the default load context. + // LAST: Register the 'Resolving' handler and 'ResolvingUnmanagedDll' handler on the default load context. AssemblyLoadContext.Default.Resolving += Resolve; + + // Add last resort native dll resolver. + // Default order: + // 1. System.Runtime.InteropServices.DllImportResolver callbacks + // 2. AssemblyLoadContext.LoadUnmanagedDll() + // 3. AssemblyLoadContext.Default.ResolvingUnmanagedDll handlers + AssemblyLoadContext.Default.ResolvingUnmanagedDll += NativeDllHandler; } #endregion Constructor @@ -194,6 +198,50 @@ namespace System.Management.Automation return null; } + /// + /// If a managed dll has native dependencies the handler will try to find these native dlls. + /// 1. Gets the managed.dll location (folder) + /// 2. Based on OS name and architecture name builds subfolder name where it is expected the native dll resides: + /// 3. Loads the native dll + /// + /// managed.dll folder + /// | + /// |--- 'win-x64' subfolder + /// | |--- native.dll + /// | + /// |--- 'win-x86' subfolder + /// | |--- native.dll + /// | + /// |--- 'win-arm' subfolder + /// | |--- native.dll + /// | + /// |--- 'win-arm64' subfolder + /// | |--- native.dll + /// | + /// |--- 'linux-x64' subfolder + /// | |--- native.so + /// | + /// |--- 'linux-x86' subfolder + /// | |--- native.so + /// | + /// |--- 'linux-arm' subfolder + /// | |--- native.so + /// | + /// |--- 'linux-arm64' subfolder + /// | |--- native.so + /// | + /// |--- 'osx-x64' subfolder + /// | |--- native.dylib + /// + internal static IntPtr NativeDllHandler(Assembly assembly, string libraryName) + { + var folder = Path.GetDirectoryName(assembly.Location); + s_nativeDllSubFolder ??= GetNativeDllSubFolderName(out s_nativeDllExtension); + var fullName = Path.Combine(folder, s_nativeDllSubFolder, libraryName) + s_nativeDllExtension; + + return NativeLibrary.Load(fullName); + } + #endregion Internal_Methods #region Private_Methods @@ -483,6 +531,34 @@ namespace System.Management.Automation throw new FileNotFoundException(message); } + private static string s_nativeDllSubFolder; + private static string s_nativeDllExtension; + + private static string GetNativeDllSubFolderName(out string ext) + { + string folderName = string.Empty; + ext = string.Empty; + var processArch = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + folderName = "win-" + processArch; + ext = ".dll"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + folderName = "linux-" + processArch; + ext = ".so"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + folderName = "osx-x64"; + ext = ".dylib"; + } + + return folderName; + } + #endregion Private_Methods } @@ -512,6 +588,3 @@ namespace System.Management.Automation } } } - -#endif - diff --git a/test/powershell/engine/Basic/Assembly.LoadNative.Tests.ps1 b/test/powershell/engine/Basic/Assembly.LoadNative.Tests.ps1 new file mode 100644 index 0000000000..854889c2d9 --- /dev/null +++ b/test/powershell/engine/Basic/Assembly.LoadNative.Tests.ps1 @@ -0,0 +1,72 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "Can load a native assembly" -Tags "CI" { + + BeforeAll { + ## The assembly files cannot be removed once they are loaded, unless the current PowerShell session exits. + ## If we use $TestDrive here, then Pester will try to remove them afterward and result in errors. + $TempPath = [System.IO.Path]::GetTempFileName() + if (Test-Path $TempPath) { Remove-Item -Path $TempPath -Force -Recurse } + New-Item -Path $TempPath -ItemType Directory -Force > $null + + $root = Join-Path $TempPath "testDllNativeFolder" + New-Item -Path $root -ItemType Directory -Force > $null + + $processArch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString().ToLower() + + if ($IsWindows) { + $arch = "win-" + $processArch + $nativeDllName = "nativedll.dll" + $sourceDllName = "hostpolicy.dll" + } elseif ($IsLinux) { + $arch = "linux-" + $processArch + $nativeDllName = "nativedll.so" + $sourceDllName = "libhostpolicy.so" + } elseif ($IsMacOs) { + $arch = "osx-" + $processArch + $nativeDllName = "nativedll.dylib" + $sourceDllName = "libhostpolicy.dylib" + } else { + throw "Unsupported OS" + } + + $archFolder = Join-Path $root $arch + New-Item -Path $archFolder -ItemType Directory -Force > $null + #New-Item -Path $archFolder\$nativeDllName -ItemType File -Force > $null + Copy-Item -Path $PSHOME\$sourceDllName -Destination $archFolder\$nativeDllName + + $managedDllPath = Join-Path $root managed.dll + + $source = @" + using System; + using System.Runtime.InteropServices; + public class TestNativeClass2 + { + public static int Add(int a, int b) + { + return (a + b); + } + + public static void LoadNative() + { + TestEntry(); + } + + [DllImport ("nativedll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void TestEntry(); + } +"@ + + Add-Type -OutputAssembly $managedDllPath -TypeDefinition $source + Add-Type -Assembly $managedDllPath + } + + It "Can load native dll" { + # Managed dll is loaded + [TestNativeClass2]::Add(1,2) | Should -Be 3 + + # Native dll is loaded from the same managed dll + { [TestNativeClass2]::LoadNative() } | Should -Throw -ErrorId "EntryPointNotFoundException" + } +}