Bending .NET - Move Native Libraries to Sub-directory After Publish

or how to make rebel humans hide due to hunting sentinels and still find them.

In this post, part of the Bending .NET series, I will cover how to move native libraries (like WPF dependencies) to a sub-directory on publish and ensure these are properly loaded during startup using NativeLibrary. The goal is to have only the single exe-file in the top-level directory and sweeping everything else under the rug in a sub-directory.

sweep under rug Source: flickr

I have created a WPF application project based on Bending .NET - Common Flat Build Output ending up with the files shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.\src\MoveNativeLibraries.AppWpf
.\src\MoveNativeLibraries.AppWpf\App.xaml
.\src\MoveNativeLibraries.AppWpf\App.xaml.cs
.\src\MoveNativeLibraries.AppWpf\AssemblyInfo.cs
.\src\MoveNativeLibraries.AppWpf\MainWindow.xaml
.\src\MoveNativeLibraries.AppWpf\MainWindow.xaml.cs
.\src\MoveNativeLibraries.AppWpf\MoveNativeLibraries.AppWpf.csproj
.\src\MoveNativeLibraries.AppWpf\Program.cs
.\src\Directory.Build.props
.\src\Directory.Build.targets
.\src\OutputBuildProps.props
.\src\OutputBuildTargets.props
.\global.json
.\MoveNativeLibraries.sln
.\publish.ps1

With the csproj-file defined initially as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <DebugType>embedded</DebugType>
    <TargetFramework>net6.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWPF>true</UseWPF>
    <UseWinForms>true</UseWinForms>
    <StartupObject>MoveNativeLibaries.AppWpf.Program</StartupObject>
    
    <PublishSingleFile>true</PublishSingleFile>
    <EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
  </PropertyGroup>

</Project>

this will output:

1
2
3
4
5
6
.\MoveNativeLibraries.AppWpf.exe
.\D3DCompiler_47_cor3.dll
.\PenImc_cor3.dll
.\PresentationNative_cor3.dll
.\vcruntime140_cor3.dll
.\wpfgfx_cor3.dll

in the flat output at build\MoveNativeLibraries.AppWpf_AnyCPU_Release_net6.0-windows_win-x64 when running:

1
2
dotnet publish --nologo -c Release -r win-x64 /p:Platform=AnyCPU \
  --self-contained true ./src/MoveNativeLibraries.AppWpf/MoveNativeLibraries.AppWpf.csproj

Now above is just an example. For a real application we have a large number of native libraries totaling a couple of GBs in size and we very much prefer these to be “hidden away” from the user in a sub-directory instead of next to the exe-file. While .NET 6 does have an option IncludeNativeLibrariesForSelfExtract as detailed in Single file deployment and executable, this means native libraries are extracted to a temporary directory that is hard to find. With a couple of GBs this can quickly add up. It also makes the executable itself much larger, which adds friction when doing updates where you don’t need to update native libraries. Note that there is an option to exclude certain files from being embedded with ExcludeFromSingleFile, as discussed in the link, but this doesn’t work for our case either.

What we really want is for the dll-files to be moved to a sub-directory that for example is named according to the RuntimeIdentifier like win-x64 so we get:

1
2
3
4
5
6
.\win-x64\D3DCompiler_47_cor3.dll
.\win-x64\PenImc_cor3.dll
.\win-x64\PresentationNative_cor3.dll
.\win-x64\vcruntime140_cor3.dll
.\win-x64\wpfgfx_cor3.dll
.\MoveNativeLibraries.AppWpf.exe

Note that you can name the sub-directory whatever you want; bin, libs or just x64 and x86 which is actually what we do due to pre-existing native library conventions. Moving the files is very easy to do with a simple target that is run after publish.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <DebugType>embedded</DebugType>
    <TargetFramework>net6.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWPF>true</UseWPF>
    <UseWinForms>true</UseWinForms>
    <StartupObject>MoveNativeLibaries.AppWpf.Program</StartupObject>
    
    <PublishSingleFile>true</PublishSingleFile>
    <EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
  </PropertyGroup>

  <Target Name="MoveNativeDllsToSubDirectory" AfterTargets="Publish">
    <PropertyGroup>
      <NativeDllSubDir>$(RuntimeIdentifier)</NativeDllSubDir>
    </PropertyGroup>
    <ItemGroup>
      <NativeDllsToMove Include="$(PublishDir)*.dll" />
    </ItemGroup>
    <Move SourceFiles="@(NativeDllsToMove)" 
          DestinationFolder="$(PublishDir)\$(NativeDllSubDir)\" />
    <Message Text="Moved native dlls to sub-directory '$(NativeDllSubDir)'" 
             Importance="high" />
  </Target>

</Project>

However, if we then try to run the executable nothing happens 🤦‍ Or to be precise, the application crashes on startup. The exception that occurs can be found with EventViewer under Windows logs -> Application and can look like (from source .NET Runtime):

1
2
3
4
5
6
7
8
9
Application: MoveNativeLibraries.AppWpf.exe
CoreCLR Version: 6.0.21.52210
.NET Version: 6.0.0
Description: The process was terminated due to an unhandled exception.
Exception Info: System.DllNotFoundException: Dll was not found.
   at MS.Internal.WindowsBase.NativeMethodsSetLastError.SetWindowLongPtrWndProc(HandleRef hWnd, Int32 nIndex, WndProc dwNewLong)
   at MS.Win32.UnsafeNativeMethods.CriticalSetWindowLong(HandleRef hWnd, Int32 nIndex, WndProc dwNewLong)
   at MS.Win32.HwndSubclass.HookWindowProc(IntPtr hwnd, WndProc newWndProc, IntPtr oldWndProc)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)

The exception message Dll was not found is not useful at all. It doesn’t say which dll was not found. As far I can tell the DllNotFoundException for P/Invoke methods has been changed in .NET 6 (Core?) to no longer include the dll file name in question, which is a bit annoying. Fortunately, the stack trace gives us a clue that this is due to WPF (WindowsBase) not being able to find a native library. It is not a big issue here since we know that we have moved some dlls and what they are, but in a large application with lots of native dependencies this can be troublesome.

Normally, I would fix this by simply adding the win-x64 sub-directory to the list of directories searched with AddDllDirectory (or SetDllDirectory) as also discussed in Load native libraries from a dynamic location. Another approach is discussed in Native bindings in C#. However, for some reason none of these work without further changes for the WPF native libraries. Instead, I had to manually pre-load the WPF native libraries using NativeLibrary.TryLoad meaning the Program.cs ends up looking like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;

namespace MoveNativeLibaries.AppWpf;

class Program
{
    [STAThread]
    static void Main()
    {
        PreloadDotnetDependenciesFromSubdirectoryManually();

        RunApp();
    }

    static void RunApp()
    {
        var app = new App();
        app.InitializeComponent();
        app.Run();
    }

    static void PreloadDotnetDependenciesFromSubdirectoryManually()
    {
        // https://www.lostindetails.com/articles/Native-Bindings-in-CSharp
        // https://www.meziantou.net/load-native-libraries-from-a-dynamic-location.htm
        // None of the above worked but approach is inspired by it.
        // First, ensure sub-directory with native libraries is 
        // added to dll directories
        var dllDirectory = Path.Combine(AppContext.BaseDirectory,
            Environment.Is64BitProcess ? "win-x64" : "win-x86");
        var r = AddDllDirectory(dllDirectory);
        Trace.WriteLine($"AddDllDirectory {dllDirectory} {r}");

        // Then, try manually loading the .NET 6 WPF 
        // native library dependencies
        TryManuallyLoad("vcruntime140_cor3");
        TryManuallyLoad("wpfgfx_cor3");
        TryManuallyLoad("PresentationNative_cor3");
        TryManuallyLoad("PenImc_cor3.dll");
        TryManuallyLoad("D3DCompiler_47_cor3");
    }

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    static extern int AddDllDirectory(string NewDirectory);

    static void TryManuallyLoad(string libraryName)
    {
        // NOTE: For the native libraries we load here, 
        //       we do not care about closing the library 
        //       handle since they live as long as the process.
        var loaded = NativeLibrary.TryLoad(libraryName, 
            Assembly.GetExecutingAssembly(),
            DllImportSearchPath.SafeDirectories | 
            DllImportSearchPath.UserDirectories,
            out var handle);
        if (!loaded)
        {
            Trace.WriteLine($"Failed loading {libraryName}");
        }
        else
        {
            Trace.WriteLine($"Loaded {libraryName}");
        }
    }
}

Note that we’ve only had to manually pre-load the WPF native libraries, all the other native libraries are loaded fine without this trick. Additionally, one could forego using AddDllDirectory and simply load the dlls directly from sub-directory e.g. by absolute path, but with the above approach we are leaving open the option of still finding the dlls in some other path.

Hence, the application now starts as expected and we can go back to hunting rebel humans.

Move Native Libraries Wpf App

PS: Example source code can be found in the GitHub repo for this blog nietras.github.io and as a zip-file MoveNativeLibraries.zip.

2022.01.03