Loading native DLLs in F# Interactive | Christoph Rüegg

Loading native DLLs in F# Interactive

F# Interactive (FSI) is a very convenient environment to execute pieces of F# code on the fly. You can even reference managed assemblies using the #r and #I preprocessor directives. However, if one of the referenced assemblies tries to use a native DLL using p/invoke you might end up with a DllNotFoundException even if the native DLL is in the same folder as the managed assembly, and if the folder has been included with the #I directive. Note that it is not possible to reference native DLLs explicitly in .Net.

The reason is that finding and loading such DLLs in .Net works the same way as all native applications in Windows and follows the standard search order. When launched from within VisualStudio, the working directory of the F# Interactive process is the path where it is installed, in my case C:\Program Files (x86)\Microsoft SDKs\F#\3.0\Framework\v4.0. Naturally it has no chance to find a DLL in your script folder and fails.

There are multiple ways how you can tell Windows where to look for the DLL:

Any technique described here is only needed when using F# Interactive or run F# scripts, but not in normal compiled applications (where you'd just tell the IDE or build tool to copy the native DLLs to the output path)

Change the Working Directory

The simplest way is to set the process' working directory to the directory where the DLL is located. If it is in the same place as your script file, you can do this with a one-liner:

1: 
System.Environment.CurrentDirectory <- __SOURCE_DIRECTORY__

Set Path Environment Variable

Beside of the working directory, Windows also considers the Path environment variables to look for the DLL. Environment variables can be defined globally or on your user account, but also locally to a running process.

You can append the path to the process-local Path variable like this:

1: 
2: 
3: 
4: 
open System

Environment.SetEnvironmentVariable("Path",
    Environment.GetEnvironmentVariable("Path") + ";" + __SOURCE_DIRECTORY__)

Tell Windows: SetDllDirectory

We can also tell Windows directly where to look by calling the SetDllDirectory function:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
open System.Runtime.InteropServices

module Kernel =
    [<DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)>]
    extern bool SetDllDirectory(string lpPathName);

Kernel.SetDllDirectory(__SOURCE_DIRECTORY__)

However, note that every time this method is called it overrides what was set in the last call. So this can work well for a while - until someone else within your process starts calling it as well.

Load Explicitly: LoadLibrary

If a library module with the same name is already loaded into memory, it will be used directly without even starting to look for it in the file system. We can leverage this by explicitly loading a library.

For example, for the MKL extensions for Math.NET Numerics we could write:

1: 
2: 
3: 
4: 
5: 
6: 
module Kernel =
    [<DllImport("Kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)>]
    extern IntPtr LoadLibrary(string lpFileName);

Kernel.LoadLibrary(Path.Combine(__SOURCE_DIRECTORY__, "libiomp5md"))
Kernel.LoadLibrary(Path.Combine(__SOURCE_DIRECTORY__, "MathNet.Numerics.MKL.dll"))

If you were writing this in a long running process where the DLL is used in a well defined section only, then you'd better unload the library once no longer needed with the symmetric FreeLibrary routine. But for quick experiments in F# Interactive it's probably fine.

Example: Enable the MKL native provider in Math.NET Numerics

Math.NET Numerics provides a few code samples as a NuGet package. Ignoring for now that we really should add much more interesting, complete and applied examples (ideas and contributions are welcome!), these examples also do not currently leverage the Intel MKL native provider for faster linear algebra, so let's change at least that. First get a copy of them:

  1. Create a new F# Console Application project
  2. Open the NuGet Package Manager Console and run

    1: 
    2: 
    3: 
    
    Install-Package MathNet.Numerics.FSharp -Version 2.6.0
    Install-Package MathNet.Numerics.FSharp.Sample -Version 2.6.0
    Install-Package MathNet.Numerics.MKL.Win-x86 -Version 1.3.0
    
  3. Open the Matrices.fsx file in Samples/MathNet.Numerics.FSharp
  4. Fix the MathNet.Numerics reference line to v2.6.1, or whatever package NuGet installed

Now we would like to enable MKL and verify that it works. The native DLLs have already been copied to the project root directory by the NuGet package in step 2.

Add the following lines right after the original 3 open-lines at the beginning of the file:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
open System
open System.IO
open MathNet.Numerics
open MathNet.Numerics.Algorithms.LinearAlgebra.Mkl

Control.LinearAlgebraProvider <- MklLinearAlgebraProvider()

let m = matrix [[1.; 2.]; [3.; 4.]]
let v = vector [4.;5.]
m.LU().Solve(v)

If you try to execute this line by line, the last line calling LU and Solve will try to use MKL and fail with a DllNotFoundException as expected. Let's try to use the environment variables approach by adding the following lines after the open-lines. Our script is two directories down from the project root directory, so we have to fix the path accordingly:

1: 
2: 
Environment.SetEnvironmentVariable("Path",
    Environment.GetEnvironmentVariable("Path") + ";" + Path.Combine(__SOURCE_DIRECTORY__,@"..\..\"))

And suddenly it works.

If it does not and you get a BadImageFormatException, you may have switched F# Interactive to run in 64-bit mode. In this case you should install the MKL.Win-x64 package instead of the x86 one.

PS: We're trying to simplify this in the upcoming v3 release so there is only one package for both platforms and it automatically loads the right one. Maybe we can also do something about this whole 'telling windows where to find the DLLs' thing while we're at it...

namespace System
type Environment =
  static member CommandLine : string
  static member CurrentDirectory : string with get, set
  static member Exit : exitCode:int -> unit
  static member ExitCode : int with get, set
  static member ExpandEnvironmentVariables : name:string -> string
  static member FailFast : message:string -> unit + 1 overload
  static member GetCommandLineArgs : unit -> string[]
  static member GetEnvironmentVariable : variable:string -> string + 1 overload
  static member GetEnvironmentVariables : unit -> IDictionary + 1 overload
  static member GetFolderPath : folder:SpecialFolder -> string + 1 overload
  ...
  nested type SpecialFolder
  nested type SpecialFolderOption

Full name: System.Environment
property System.Environment.CurrentDirectory: string
Environment.SetEnvironmentVariable(variable: string, value: string) : unit
Environment.SetEnvironmentVariable(variable: string, value: string, target: EnvironmentVariableTarget) : unit
Environment.GetEnvironmentVariable(variable: string) : string
Environment.GetEnvironmentVariable(variable: string, target: EnvironmentVariableTarget) : string
namespace System.Runtime
namespace System.Runtime.InteropServices
Multiple items
type DllImportAttribute =
  inherit Attribute
  new : dllName:string -> DllImportAttribute
  val EntryPoint : string
  val CharSet : CharSet
  val SetLastError : bool
  val ExactSpelling : bool
  val PreserveSig : bool
  val CallingConvention : CallingConvention
  val BestFitMapping : bool
  val ThrowOnUnmappableChar : bool
  member Value : string

Full name: System.Runtime.InteropServices.DllImportAttribute

--------------------
DllImportAttribute(dllName: string) : unit
type CharSet =
  | None = 1
  | Ansi = 2
  | Unicode = 3
  | Auto = 4

Full name: System.Runtime.InteropServices.CharSet
field CharSet.Auto = 4
type bool = Boolean

Full name: Microsoft.FSharp.Core.bool
val SetDllDirectory : lpPathName:string -> bool

Full name: loadingnativedllsinfsharpinteractive.Kernel.SetDllDirectory
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = String

Full name: Microsoft.FSharp.Core.string
val lpPathName : string
module Kernel

from loadingnativedllsinfsharpinteractive
Multiple items
type IntPtr =
  struct
    new : value:int -> nativeint + 2 overloads
    member Equals : obj:obj -> bool
    member GetHashCode : unit -> int
    member ToInt32 : unit -> int
    member ToInt64 : unit -> int64
    member ToPointer : unit -> unit
    member ToString : unit -> string + 1 overload
    static val Zero : nativeint
    static member Add : pointer:nativeint * offset:int -> nativeint
    static member Size : int
    ...
  end

Full name: System.IntPtr

--------------------
IntPtr()
IntPtr(value: int) : unit
IntPtr(value: int64) : unit
IntPtr(value: nativeptr<unit>) : unit
namespace System.IO
namespace System.Numerics
namespace Microsoft.FSharp.Control
val m : obj

Full name: loadingnativedllsinfsharpinteractive.m
val v : obj

Full name: loadingnativedllsinfsharpinteractive.v
type Path =
  static val DirectorySeparatorChar : char
  static val AltDirectorySeparatorChar : char
  static val VolumeSeparatorChar : char
  static val InvalidPathChars : char[]
  static val PathSeparator : char
  static member ChangeExtension : path:string * extension:string -> string
  static member Combine : [<ParamArray>] paths:string[] -> string + 3 overloads
  static member GetDirectoryName : path:string -> string
  static member GetExtension : path:string -> string
  static member GetFileName : path:string -> string
  ...

Full name: System.IO.Path
Path.Combine([<ParamArray>] paths: string []) : string
Path.Combine(path1: string, path2: string) : string
Path.Combine(path1: string, path2: string, path3: string) : string
Path.Combine(path1: string, path2: string, path3: string, path4: string) : string