Powershell: Loading Native Libraries is broken

Created on 27 Apr 2019  ·  70Comments  ·  Source: PowerShell/PowerShell

Attempting to load a module that requires p/invoke from a third-party native library fails to load the library from the same folder as the DLL doing the p/invoking.

This _used to work_ in PowerShell Core 6.1.0 but regressed between 6.2.0-preview1 and 6.2.0-preview2. It still works in Windows PowerShell.

The native library it's failing to locate is located in the same folder as the SkiaSharp.dll that is trying to locate it. See https://github.com/PowerShell/PowerShell/issues/8861#issuecomment-462362391 and that issue thread for why it was done in that fashion.

Steps to reproduce

Install-Module PSWordCloud
New-WordCloud

Expected behavior

cmdlet New-WordCloud at command pipeline position 1
Supply values for the following parameters:
InputObject:

Actual behavior

new-wordcloud : The type initializer for 'PSWordCloud.WCUtils' threw an exception.
At line:1 char:1
+ new-wordcloud
+ ~~~~~~~~~~~~~
+ CategoryInfo          : NotSpecified: (:) [], TypeInitializationException
+ FullyQualifiedErrorId : System.TypeInitializationException

Querying for the base exception with $Error[0].Exception.GetBaseException() | Format-List * -F yields the following:

Message        : Unable to load DLL 'libSkiaSharp' or one of its dependencies: The specified module could not be
                 found. (Exception from HRESULT: 0x8007007E)
TypeName       :
Data           : {}
InnerException :
TargetSite     : IntPtr sk_fontmgr_ref_default()
StackTrace     :    at SkiaSharp.SkiaApi.sk_fontmgr_ref_default()
                    at SkiaSharp.SKFontManager.get_Default()
                    at PSWordCloud.WCUtils..cctor()
HelpLink       :
Source         : SkiaSharp
HResult        : -2146233052

Environment data

Name                           Value
----                           -----
PSVersion                      6.2.0
PSEdition                      Core
GitCommitId                    6.2.0
OS                             Microsoft Windows 10.0.17763
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

_Note:_ The same was tested in a Mac environment (thanks @steviecoaster!) and also failed in the same way. I suspect it will be identical on Linux distros as well.

/cc @SteveL-MSFT

Issue-Question Resolution-Fixed

Most helpful comment

@rkeithhill

I really wish I didn't have to recompile this for each platform because this is netstandard2 assembly. But AFAICT the lib name value provided to the DllImport attribute has to be a compile time constant. :-(

FWIW you can omit the extension for DllImport. We do that for PSES's non-Windows native dependency. Doesn't help if the base names aren't the same though.

You can also probably skip the compiler directives for the LoadLibrary/dlopen calls by storing them in separate interface implementations. As long as the method isn't JIT'd it shouldn't throw at runtime.

All 70 comments

@adityapatwardhan can you take a look at this?

You could look used paths with procmon utility and then try to fix with HintPath

I'll give that a look but given this isn't a library I'm loading directly (it's loaded by the main SkiaSharp.dll itself) I'm not that confident in that doing too much. The fact that there are four possible library files to load depending on platform also complicates things.

But regardless, this doesn't seem like an intentional change so the regression should probably be addressed.

MSFT team works hard on vNext p/invokes in .Net Core 3.0 and perhaps they back-ported something to 2.1. I don't remember that we changed anything in PowerShell related dll loading.

There was #8073 and several Internal-marked changes in 6.2.0-preview2. I have no idea what else may have changed during those. 😕

The PR change is to load module dll from module folder first then from GAC. Your problem is that the module dll reference second dll and auto dll loading (it does Core not PowerShell ) doesn't found the dll. My suggestion is to explicitly add hintpath to the dll in project file.

@iSazonov I added some HintPath references to my csproj, but all I get is a warning when I dotnet publish. There is no change in behaviour of the module once loaded.

From a bit of reading up on it, it looks like the HintPath is used to reference external assemblies that _aren't included_ in the project build itself. The SkiaSharp assemblies are included, so adding a HintPath for compile/build doesn't affect the Skia DLLs or the paths they look up at runtime.

For reference, this is my csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="All" />
    <PackageReference Include="SkiaSharp" Version="1.68.0" />
    <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.0" />
  </ItemGroup>

</Project>

I added the following lines to no effect (other than a build/publish time warning that they can't be found, because, well, they don't exist until after the publish is completed anyway):

  <ItemGroup>
    <Reference Include="libSkiaSharp">
        <HintPath>win-x86/libSkiaSharp.dll</HintPath>
        <HintPath>win-x64/libSkiaSharp.dll</HintPath>
        <HintPath>osx/libSkiaSharp.dylib</HintPath>
        <HintPath>linux-x64/libSkiaSharp.dylib</HintPath>
    </Reference>
</ItemGroup>

From what I've read on HintPath, it doesn't seem to have any effect on what paths are searched when loading a DLL, or that DLL tries to load other DLLs for p/invoke purposes.

_That_ HintPath is a compile-time thing only.

The way you say that makes it sound like there's another I missed?

@vexx32 If you have simple repo code you could ask in CoreCLR repository.

I'm unsure if I can rule out a Powershell-specific change just yet. I might have to put a small test together for a console app and see how that goes.

Thanks to a tip from @Jaykul, I've managed to put together a stop-gap solution. Executing this from the PSM1 before importing the cmdlet and the main Skia DLL seems to resolve the issue.

Add-Type -TypeDefinition @"
    using System.Runtime.InteropServices;

    public class DllLoadPath
    {
        [DllImport("kernel32", CharSet=CharSet.Unicode)]
        public static extern int SetDllDirectory(string NewDirectory);
    }
"@

# ...

[DllLoadPath]::SetDllDirectory($NativeRuntimeFolder)

However, the fact that it _requires_ additional p/invoke to make this work is kind of ridiculous. I'm also very confused as to what changed and removed the loaded SkiaSharp.dll's own directory path from the standard search locations as documented here.

@vexx32 I was not able to reproduce the issue. Did I miss something in the repro? Maybe a specific version if PSWordCloud?

PS> Install-Module PSWordCloud                                                               
Untrusted repository
You are installing the modules from an untrusted repository. If you trust this repository, change its
InstallationPolicy value by running the Set-PSRepository cmdlet. Are you sure you want to install the modules from
'PSGallery'?
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "N"): y

PS> New-WordCloud                                                                            
cmdlet New-WordCloud at command pipeline position 1
Supply values for the following parameters:
InputObject:
Path:

PS> $PSVersionTable                                                                          
Name                           Value
----                           -----
PSVersion                      6.2.0
PSEdition                      Core
GitCommitId                    6.2.0
OS                             Microsoft Windows 10.0.18885
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Sorry, as mentioned I rectified it in 2.1.1.

If you install 2.1.0 specifically you'll see the failure, as it's lacking the above p/invoke workaround.

Ok, I can repro now. I will have a look.

@vexx32 It seems that we load all the file listed in FileList of the psd1. Do you need to have that? When I removed all the entries from FileList I cannot repro the issue any more.

Is that so? That's very strange. No, I don't particularly need that and probably will remove it anyway as I don't really want to have to constantly update it, but that has been there for a few versions.

Seems weird that that would affect DLL loading all of a sudden, though.

It seems that we load all the file listed in FileList of the psd1.

We have an issue to support "if" in .psd1 files. So we could load platform specific dlls.

@iSazonov it's #5541. I will give you update soon.

That is a possibility. What I'm wondering _now_ is if simply adding the path to the native libraries (all of them) in the FileList is sufficient here... I might not even need the p/invoke. I'll do some testing! 😊

EDIT: Nope, I'm not seeing that have any effect. :(

We call Assembly.Load() that only loads managed assemblies.

We made a change in 6.2.0-preview.2 where we started loading DLLs from module folder before looking up in the GAC. The side effect of that change is we also load DLLs in FileList.

That makes sense. Do we have a doc issue open for that? It seems like this sort of change that will be very hard to track down if someone runs into it and we haven't documented it. We should probably document that you don't want to put native assemblies in the filelist for that reason.

Appreciate the help tracking it down! For now the P/invoke solution I mentioned gives me the most flexibility, but it might be sensible to add handling for such things in module loading.

For example, let's say in PrivateData we add an entry called RuntimeLibPaths, which is a hashtable where the keys match supported platform names / architectures, and the values are the path to the folder containing the native library. The appropriate folder(s) are added to the dll search paths on import by PowerShell internally rather than having to be added by the module itself on import.

Do we have a doc issue open for that?

We haven't.

@adityapatwardhan @SteveL-MSFT thanks to @TylerLeonhardt (unintentionally) reminding me that this has to work on non-Windows things too if I want to be thorough... I need a more complete solution.

There currently does not appear to be any clear documentation on how native interop is supposed to be handled in .NET Core itself. All I can find is this page for .NET Framework in MS docs.

Is it handled via Mono or something? If it's via Mono, looking at this page there is actually a _bug in Mono_ on Mac OS that prevents the library load paths being properly respected...

PowerShell... needs to handle this. I'm not sure there _is_ a way to handle this appropriately from all platforms from the module itself.

@vexx32 don't know the details, if you can produce a repro in C# that doesn't include PowerShell, I would suggest opening an issue in the CoreCLR repo

@steveL-msft that's the issue, I think. It seems to work fine elsewhere, with some caveats still. For example, loading the native lib works if it's in the same folder as the main SkiaSharp.dll, but for PS that doesn't work due to the change @adityapatwardhan mentions. That just multiplies the difficulty in the case of working with PowerShell and how it handles modules.

In most situations in working with such native libraries it can be abstracted away, because you can just package it for each supported platform anyway. We quite simply don't have that luxury for PS modules, unless I want to package and maintain three or more completely separate gallery modules, which is unfortunately really bad for discoverability and the end user experience.

Given that PS is a cross platform tool, we need some way to handle native libraries in modules, or we need some way to have the PackageManagement module recognise that there need to be separate libraries extracted on a per-platform basis from the nupkg.

I'm open to alternate solutions, but it seems the deck here is pretty scarce. :confused:

@vexx32 I think you want something like this https://github.com/PowerShell/PowerShellGet/issues/273

In CoreFX 3.0 we get some new APIs for pinoke native libraries. They are only at the beginning of the road, but they want to do something even better than Mono.

Currently my solutionworkaround is looking like this:

  1. Native libraries for Mac and Linux are in the module root, as that seems to be the only place I can put them and have them reliably loaded at the moment.
  2. A ScriptsToProcess handles p/invoke on module import to add either the 32 or 64-bit version of the Windows native library, which must be stored in subfolders as the filenames are, unfortunately, identical.

Not ideal, and as @TylerLeonhardt can attest from his stream today, this still doesn't work for some reason when running in Azure Functions at the moment.

@vexx32 don't know the details, if you can produce a repro in C# that doesn't include PowerShell, I would suggest opening an issue in the CoreCLR repo

Nope: In C# this isn't a problem

In C# you either:

  1. Develop an app, and release _separate installers_ per platform, and have only _one library search path_ to worry about.
  2. Develop a library, and release _NuGet packages_, and nuget gives you a path pattern telling you where to put your libraries, and then studio/msbuild takes care of everything for you.

@vexx32 I think you want something like this PowerShell/PowerShellGet#273

Nope: This isn't a cross-platform problem

That's why this bug was filed. Ever since PowerShell 6.2 @vexx32 can't even get a version that works in _just Windows_ because he can't figure out how to get PowerShell to use the right library search path.

In Windows at least there's a p/invoke available that lets me mess around with it. That's a _workaround_ but really we need a better fix.

On Unix systems, however, there _is no such p/invoke_ available, from what I've been able to find, and for whatever reason PowerShell isn't querying the environment variables that normally indicate which paths to search when you try to DllImport from an assembly loaded by PS.

So on basically all platforms this is... extremely unfriendly to do anything with. 😅

As it currently stands there appears to be no other option apart from including any and all Unix native assemblies directly in the module root folder (which limits the platforms you can support, as often there can be name collisions), and then being careful about which folder you add to the search paths on Windows with the p/invoke (if you care about supporting x86 Windows -- and @TylerLeonhardt tells me that Azure Functions runs on x86 for some strange reason, so that's a necessity if you want to work with those.)

This is what I use in my psm1 file to pre-load the appropriate native binary for the platform. Note: I only targeted/tested this on Linux and Windows:

# Module PSM1 File
$binPath = Join-Path $PSScriptRoot bin $(if ($IsWindows) {"Windows"} else {"Linux"})
Add-Type -Path $binPath\Acme.Api.dll

And this is the C# code that goes in Acme.Api.dll

[Flags]
public enum DLOpenFlags
{
    RTLD_LAZY = 1,
    RTLD_NOW = 2,
    RTLD_LOCAL = 4,
    RTLD_GLOBAL = 8,
}

public static class LinuxNativeMethods
{
    [DllImport("libdl.so", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr dlopen(
        string dlToOpen, 
        DLOpenFlags flags);
}

public static class Win32NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint="LoadLibraryW")]
    public static extern IntPtr LoadLibrary(
        [InAttribute, MarshalAs(UnmanagedType.LPWStr)] string dllToLoad);
}

public static class FooNativeMethods
{
    // On Linux/macOS the shared lib must be named Foo.so and not libFoo.so
    public const string LibName = "Foo";

    static FooNativeMethods()
    {
        string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 
        {
            string nativeDllPath = Path.Combine(assemblyDir, "Windows", $"{LibName}.dll");
            LibHandle = Win32NativeMethods.LoadLibrary(nativeDllPath);
        }
        else
        {
            string nativeSOPath = Path.Combine(assemblyDir, "Linux", $"{LibName}.so");
            LibHandle = LinuxNativeMethods.dlopen(nativeSOPath, DLOpenFlags.RTLD_LAZY);
        }
    }

    public static IntPtr LibHandle { get; private set; }

    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int getLastError(
        StringBuilder buffer,
        ref int bufferLength);

    ...
}

@vexx32 You could open consulting issue in CoreCLR repo. I guess they have that to say.

@rkeithhill this only works for .NET binaries. I need to load a .NET binary that itself needs to use P/invoke to load a native assembly. That is where it fails and falls apart completely. I have no control over this library, it is a third party library (SkiaSharp, in this instance). I am unable to load this library in my binary module as it needs to be loaded at the same time as code is loaded from my module, as my module is dependent on types only present and functional with SkiaSharp loaded.

@isazonov as has been covered a couple times in this issue now, this isn't an issue in .net Core because you can simply generate separate packages per platform for most applications.

PowerShell doesn't have any possible way to do this, and it incidentally removes some standard paths from the library search paths, making it literally impossible to work with native assemblies in some contexts, and very fragile and particular in the ones that do happen to work.

@vexx32

I need to load a .NET binary that itself needs to use P/invoke to load a native assembly. That is where in fails and falls apart completely.

Uh, that is exactly what this code does. Been using it successfully on 6.2 on both Windows and Linux for a while now.

Amended, sorry, I wasn't clear. I can't do this in my own binary, the types need to be present as my own module is being loaded; I can't have it import during the command instantiation.... And I shouldn't have to.

PowerShell shouldnt be less functional than .net core when it comes to loading binaries.

That said, I'll see if I can adapt some things and improve my workarounds. Appreciate the examples!

But really... I don't see why I should have to go to such lengths to work around this. It should just work, and until PowerShell gets involved... It does.

Agreed on the hoops being a pain. Was just trying to show how the native lib loading can be done via p/invoke on Windows and Linux. :-)

@vexx32 I see splitting in the discussion. Could you clarify that is a problem - you have a module with native libraries and you need load right library with dependences on specific platform?

Sort of. This is the layout I have that currently works, but is messy as all heck. Anything I can't define a path for via p/invoke must be in the module root (which runs into name collision problems on some platforms).

PSWordCloud/
|- PSWordCloudCmdlet.dll
|- SkiaSharp.dll
|- libSkiaSharp.so
|- libSkiaSharp.dylib
|- win-x86/
  |- libSkiaSharp.dll
|- win-x64/
  |- libSkiaSharp.dll

SkiaSharp.dll is .NET, and is a referenced assembly for my main cmdlet dll. The four libSkiaSharp files are native per-platform DLLs. I don't control the code in SkiaSharp.dll, so I can't force that DLL to update the paths before it p/invokes.

So... I have a module with a .NET dependency. This dependency depends on a native library that it is loading with DllImport; the exact file is different per platform/architecture (but several of the files have name collisions, so that restricts the platforms I can actually support).

Using @rkeithhill's code, I think I can possibly move the Linux native library to a subfolder as well. Not sure about MacOS, there's probably something similar-ish, but I have yet to find any documentation on methods for that.

_Ideally_, ALL of these files should be at worst in a subfolder out of the way, something like /PSWordCloud/runtime/<platform>/ -- unfortunately, due to how PowerShell has changed the path loading specifications from the standard settings, only files in the root folder of the module can be loaded via DllImport. Other locations may work, but (at least on Mac/Linux) _all_ of the environment variables on those platforms that are supposed to define native runtime load paths are completely ignored by PowerShell.

I believe $env:Path still works (though it shouldn't be using that on Unix platforms, and I haven't tested that since the 6.2 release, so that might be broken as well), but frankly I don't think it's a good idea at all to add a ton of paths to that variable, it makes for a really terrible user experience if the user needs to use that environment variable at all. You load a module, and suddenly your path variable has extra unexpected data that probably shouldn't be there, but _has_ to be there for things to work correctly.

I think this should be a non-issue; PowerShell should have a more robust method of handling native library load paths if it's going to be overriding .NET Core's predefined methods. I would suggest having some way to specify a native library load path in the module manifest or something along those lines, some way to define where it should be looking per platform when something needs to p/invoke some native code.

I have the same issue and @vexx32's report contains all elements in the chain.
I'm agree PowerShell should have a more robust method of handling native library.

I don't think it's a good idea at all to add a ton of paths to that variable

Agreed and likewise, I'm not keen on using SetDllDirectory because that is also (pwsh) process wide. The module manifest could be updated to allow module authors to provide a list of native dlls per os-arch combo and then pwsh would pre-load those native dlls before it pre-loaded any assemblies. That said, if someone is going to use your assembly from another language, the assembly really should pre-load its own native dlls. The user of the assembly shouldn't have to do that IMO.

@vexx32 I "think" dlopen works on macOS as well.

I'll give it a go when I can (mac VMs are a bit... weird heh) but from memory, Mac doesn't use .so files so I'd definitely have to give it a try -- most of the native libs I see references to on Macs seem to be .dylib.

The assembly does its own preloading here (at least, I'm pretty sure it does...), I just have to special-case everything because the assembly doesn't know where to look with how PS handles paths. :/

I think macOS uses .dylib so you'd need to compile that assembly three times with different macros defined e.g.:

public static class FooNativeMethods
{
    // This unfortunately requires that you compile this assembly twice - once with WIN32 defined and again with
    // it not defined.
#if WIN32        
    public const string LibName = "Foo";
#elif MACOS
    public const string LibName = "libFoo.dylib";
#else
    public const string LibName = "libFoo.so";
#endif      

I really wish I didn't have to recompile this for each platform because this is netstandard2 assembly. But AFAICT the lib name value provided to the DllImport attribute has to be a compile time constant. :-(

Yeah. Since this is a PowerShell module, what I'd actually end up doing is a dynamic on-import compilation with Add-Type -TypeDefinition $srcString with a platform check and then point it at the correct directory. You can use ScriptsToProcess in a module manifest for this, thankfully.

Also, is libdl.so a valid and usable library to do this with on MacOS?

But yep, we need a better solution. 😄

@vexx32 Did you look Mono MapDll? The feature allows automatic mapping to appropriate native library based on conditions (arch, platform and etc.). I'd expect that it is that you need. Perhaps SkiaSharp.dll should be recompiled but it can be configured in external config file. Something similar will be implemented in Core.

If I recall correctly, .NET Core doesn't use Mono when running on Windows, so it again would only be a partial solution.

I appreciate very much all the options being offered here, and I will thoroughly examine what my current options are -- but at the end of the day, PowerShell needs to have something in place for cross-platform modules to use for this purpose going forward. 🙂

If I recall correctly, .NET Core doesn't use Mono when running on Windows, so it again would only be a partial solution.

I say about new features. See
Dllmap design document https://github.com/dotnet/coreclr/blob/c3e1bd5ccc482c9a7670762676bda95ebd34707d/Documentation/design-docs/dllmap.md
and previous version of the document
https://github.com/dotnet/coreclr/blob/6a48f05c77e80b604d71a9b84cc6b014306e849e/Documentation/design-docs/dllmap.md#

Update: CurrentContextualReflectionContext
https://github.com/dotnet/designs/blob/master/accepted/runtime-binding.md#rollforward

@rkeithhill

I really wish I didn't have to recompile this for each platform because this is netstandard2 assembly. But AFAICT the lib name value provided to the DllImport attribute has to be a compile time constant. :-(

FWIW you can omit the extension for DllImport. We do that for PSES's non-Windows native dependency. Doesn't help if the base names aren't the same though.

You can also probably skip the compiler directives for the LoadLibrary/dlopen calls by storing them in separate interface implementations. As long as the method isn't JIT'd it shouldn't throw at runtime.

I found a possible solution for this problem. I would have to try it out though. I am currently busy working on some releases, so most likely will get to this early next week. I will update this issue with my findings.

https://docs.microsoft.com/en-us/dotnet/api/system.runtime.loader.assemblyloadcontext.loadunmanageddll?view=netcore-2.2#System_Runtime_Loader_AssemblyLoadContext_LoadUnmanagedDll_System_String_

@adityapatwardhan That looks like a cross-platform wrapper around LoadLibrary/dlopen. Nice find!

@adityapatwardhan I found this issue around LoadUnmanagedDll . Maybe irrevelant to our case.

We should consider adding a callback that allow folks to supply custom resolution policy for unmanaged libraries. We have intended to do that with AssemblyLoadContext.LoadUnmanagedDll. At the end, it only addressed a small fraction of them problem because of it does not work for default load context. When we have introduced AssemblyLoadContext.LoadUnmanagedDll originally, we allowed overriding the default load context as well but then removed this capability and did not think through the impact on all scenarios.

Source : LoadUnmanagedDllFromPath / LoadUnmanagedDll is not working on linux if .so file is present in any other path other than application output directory

I think my link above on NativeLibrary APIs gives us cross-platform solution (after moving to .Net Core 3.0).

CurrentContextualReflectionContext https://github.com/dotnet/coreclr/blob/master/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md

It is for plug-in models. This could resolves the issue, #6724, #6426, and some other.
/cc @daxian-dbw

@adityapatwardhan any update since last month? We want to publish the right doc guidance on this

Have not been able to focus on this, am currently busy with other high priority items. I will update by sometime next week.

@adamdriscoll might be able to give some advice since he probably had to do some creative stuff in UniversalDashboard and AvaloniaUI +PowerShell.

FWIW the code I presented above is what we use for an internal module that uses a native lib. It runs without issues on Windows and Linux. Haven't tested it on macOS but other than tweaks to the dlopen pinvoke and changing the name of the shared lib on mac, it should work. This is on .NET Core 2.2.

UD is doing pretty much the same thing as @rkeithhill's solution. This works on MacOSX as well as Linux and Windows. https://github.com/ironmansoftware/universal-dashboard/blob/master/src/UniversalDashboard/Server/CustomAssemblyLoadContext.cs

The issue is that there could be many more variations of the folder paths to load the various binaries so it's a bit of a kluge. If a new binary type is output based on a new dependency, a new folder may pop up and UD won't load the assembly.

@rkeithhill are you merging all the WindowsLinux binaries into a single folder? (EDIT: Actually, I get it. You have a wrapper DLL assembly that just loads the target native assembly. Do you do this for all your native assemblies?)

UD just keeps the publish runtime folder the same as when it was produced with dotnet publish and then the referenced class tries to load all the native binaries based on the correct platformarchitecture.

It works but I wish PS could just look at a runtime folder and load the correct binaries. Seems like dotnet must be doing that when it starts up so maybe there is some code to nab there.

I change where I look for native assemblies based on the result RuntimeInformation.IsOSPlatform(OSPlatform.Windows). If that returns true, I load from the Windows folder of the dir the C# assembly is located in. Otherwise, I load from the Linux folder.

Do you do this for all your native assemblies?)

Yes but we've only got the one and it doesn't depend on anything else other than system libs (msvcrt, etc).

Can we have some standard file structure (maybe based on runtime names)? This would allow us to enhance the native dll resolver.

UD just keeps the publish runtime folder the same as when it was produced with dotnet publish and then the referenced class tries to load all the native binaries based on the correct platformarchitecture.

So if I'm understanding correctly, you're avoiding using PowerShell to load the native binaries by just having .NET load them under the hood when you load a managed binary that depends on the RID-specific native binaries?

It works but I wish PS could just look at a runtime folder and load the correct binaries. Seems like dotnet must be doing that when it starts up so maybe there is some code to nab there.

Yeah, I suspect there's some RID awareness happening within the .NET runtime itself. I've been trying to avoid us having to go down that path, but it sounds like we may need to at some point.

@joeyaiello

So if I'm understanding correctly, you're avoiding using PowerShell to load the native binaries by just having .NET load them under the hood when you load a managed binary that depends on the RID-specific native binaries?

Yeah, exactly. UD just loads everything for the target RID. So it's not really smart about what the actual dependencies are. Not sure if there is a good way to even detect that.

@vexx32 I installed PSWordCloud. Is the folder structure general? If so we can make our resolver more smart. In the case I want to ask you - could you please prepare new Pester test(s) based on simplified PSWordCloud (it is enough for us to get something simple from the native libraries like "Hello World")? We would merge that as pending and it will open the way to work on our resolver. Thanks!

Currently the folder structure of PSWordCloud is not general, no. That can be changed, of course.

Yeah, I can absolutely put together a simple test bed for that. Pester is a bit tricky to do, just based on the nature of the output I suppose, but I _think_ when it fails it produces an exception you can catch, though that exception is thrown from the type initializer if I recall correctly.

Would it be appropriate to have the test itself pull the Skia DLLs via dotnet, or would it be better to package them in the repo? I guess if we pull them each time it's not so big a deal if it's flagged as a feature test once it's no longer pending? :thinking:

If you could implement a test nodule with indirect dependence with Skia dlls it would be nice. And yes we could install Skia from nuget. Although the test would be more reliable if we directly put the dlls with the test module in ~our test module folder~ an assert folder.
It seems Nuget best practice is to put native dlls in RID named folders. Yes?

Yep, I can do that! I'll have a look at it tommorow. :blush:

I updated my comment - please put the test module in an test assert folder.

@rjmholt I made some attempts to use the code you provided to make it work for PSWordCloud, but it's still not working at all. The load operation completes successfully, but loading the managed SkiaSharp.dll after the unmanaged library still fails to make use of the loaded DLL on Unix platforms. It does manage to work for Windows, though, somehow.

See my WIP branch here https://github.com/vexx32/PSWordCloud/tree/FixPInvokes -- run build.ps1 to build the module and put all the files in the right places. It'll import the module for you, too. Calling New-WordCloud after running the build works on Windows, but fails completely on Mac OS.

@SeeminglyScience you see anything I did wrong there? :/

GitHub
Create pretty word clouds with PowerShell! Contribute to vexx32/PSWordCloud development by creating an account on GitHub.

@SteveL-MSFT what's the plan for handling native libs at present? 😕

:tada:This issue was addressed in #11032, which has now been successfully released as v7.0.0-rc.1.:tada:

Handy links:

Was this page helpful?
0 / 5 - 0 ratings