Skip to content
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
Comment thread
daxian-dbw marked this conversation as resolved.
Comment thread
iSazonov marked this conversation as resolved.
Comment thread
daxian-dbw marked this conversation as resolved.
Comment thread
daxian-dbw marked this conversation as resolved.
Comment thread
iSazonov marked this conversation as resolved.
Comment thread
iSazonov marked this conversation as resolved.
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Comment thread
iSazonov marked this conversation as resolved.
Comment thread
iSazonov marked this conversation as resolved.
Comment thread
iSazonov marked this conversation as resolved.
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Comment thread
iSazonov marked this conversation as resolved.
Comment thread
iSazonov marked this conversation as resolved.
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
Comment thread
iSazonov marked this conversation as resolved.
Outdated
// 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
{
Expand Down Expand Up @@ -46,7 +41,9 @@ internal static PowerShellAssemblyLoadContext InitializeSingleton(string basePat
lock (s_syncObj)
{
if (Instance != null)
{
throw new InvalidOperationException(SingletonAlreadyInitialized);
}

Instance = new PowerShellAssemblyLoadContext(basePaths);
return Instance;
Expand Down Expand Up @@ -93,8 +90,15 @@ private PowerShellAssemblyLoadContext(string basePaths)
_availableDotNetAssemblyNames = new Lazy<HashSet<string>>(
() => new HashSet<string>(_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
Expand Down Expand Up @@ -194,6 +198,50 @@ internal IEnumerable<Assembly> GetAssembly(string namespaceQualifiedTypeName)
return null;
}

/// <summary>
/// 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
Comment thread
daxian-dbw marked this conversation as resolved.
/// | |--- native.so
/// |
/// |--- 'linux-x86' subfolder
/// | |--- native.so
/// |
/// |--- 'linux-arm' subfolder
/// | |--- native.so
/// |
/// |--- 'linux-arm64' subfolder
/// | |--- native.so
/// |
/// |--- 'osx-x64' subfolder
/// | |--- native.dylib
/// </summary>
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
Expand Down Expand Up @@ -483,6 +531,34 @@ private void ThrowFileNotFoundException(string errorTemplate, params object[] ar
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
}

Expand Down Expand Up @@ -512,6 +588,3 @@ public static void SetPowerShellAssemblyLoadContext([MarshalAs(UnmanagedType.LPW
}
}
}

#endif

72 changes: 72 additions & 0 deletions test/powershell/engine/Basic/Assembly.LoadNative.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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"
Comment thread
daxian-dbw marked this conversation as resolved.
}
}