Skip to content

Using Native Libraries

lemming104 edited this page Oct 10, 2024 · 7 revisions

In .NET, it is possible to use native libraries for any supported target platform by writing or generating a Platform Invoke (P/Invoke) wrapper.

Portability Considerations

On both Windows and Linux, libraries (.DLLs on Windows, .so files on Linux) tend to reference other libraries. This can be problematic if you are trying to ship dependencies that otherwise are unlikely to exist on the target machine.

Where possible, you will probably want to either compile third-party libraries with options that minimize unnecessary dependencies, or statically link copies of those dependencies. These topics are beyond the scope of this document, as they might vary greatly between build systems and require knowledge of native tools. Additionally, some libraries might dynamically attempt to load their dependencies, and there is no one-size-fits-all approach to finding these.

Windows (Visual Studio required)

  1. From a Visual Studio command line, run dumpbin /DEPENDENTS <your file>. The resulting listing will contain the files on which the library depends.
  2. Repeat (1) for each dependency (if you can find them) to build a map of the full set of dependencies.
  3. Consider shipping these dependencies in the same directory as the libraries that reference them. Windows' default loading behavior will search this directory first, and will automatically load these files.

Example:

Dump of file fluidsynth.dll

File Type: DLL

  Image has the following dependencies:

    DSOUND.dll
    WINMM.dll
    ole32.dll
    WS2_32.dll
    glib-2.0-0.dll
    sndfile.dll
    KERNEL32.dll
    USER32.dll
    MSVCP140.dll
    VCRUNTIME140.dll
    VCRUNTIME140_1.dll
    api-ms-win-crt-string-l1-1-0.dll
    api-ms-win-crt-convert-l1-1-0.dll
    api-ms-win-crt-stdio-l1-1-0.dll
    api-ms-win-crt-math-l1-1-0.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    api-ms-win-crt-heap-l1-1-0.dll
    api-ms-win-crt-utility-l1-1-0.dll
    api-ms-win-crt-environment-l1-1-0.dll

...

In this case, many of the dependencies are either from Windows itself (KERNEL32.dll) or from a runtime the user can reasonably be expected to install (MSVCP140.dll and the various api-ms-win-crt DLLs). glib-2.0-0.dll and sndfile.dll, however, are worth including along with the main DLL we're shipping, as are any "non-platform" libraries upon which they might depend.

Linux

  1. In most Linux environments, we can use the ldd command to list a native library's dependencies.
  2. Unlike Windows, Linux searches /lib first and does not generally consider "same directory" loading. Transitive dependencies will either need to be statically linked into the parent library, or some linker trickery may be needed when compiling. If building using CMAKE, try setting the CMAKE_SHARED_LINKER_FLAGS variable to -Wl,-rpath='$ORIGIN'. Then, use ldd to list the dependencies. Copy those dependencies to your output directory and run ldd another time against the library you built, to see if it now points to the nearest copies instead of the ones in /lib/. This will usually at least help with first-level dependencies, but may not be helpful if there are multiple layers of recursive dependencies.

Example:

        linux-vdso.so.1 (0x00007ffe761b2000)
        libglib-2.0.so.0 => /lib/x86_64-linux-gnu/libglib-2.0.so.0 (0x00007f24fdc45000)
        libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f24fd9c8000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f24fd8df000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f24fd8b2000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f24fd6a0000)
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f24fd604000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f24fde70000)

Generating Wrappers

To programmatically generate wrappers for native libraries, you'll need a copy of the library itself, as well as its header file. See this page for details. The short version:

  1. Install the ClangSharp .NET tool. Note that using Winget to install the LLVM package may not be necessary if you have the Visual Studio native C/C++ installed.
dotnet tool install --global ClangSharpPInvokeGenerator
winget search clang # find the clang compiler
winget install LLVM.LLVM --version 13.0.0 # we found it, the package is called LLVM.LLVM
  1. Use ClangSharp to generate a wrapper for your library
ClangSharpPInvokeGenerator 
    --file <path to your input header file>
    -c multi-file generate-file-scoped-namespaces generate-helper-types
    -n <namespace to use for generated bindings>
    --methodClassName <class name to use for generated methods>
    --libraryPath <name of the DLL>
    -o <output path for generated files>

Load Interception

Oftentimes, libraries are named differently on Windows and Linux, or there are other reasons why a native DLL might not load properly at runtime. One method for handling this is load interception via a RegisteredDllResolver.

For an example of this, see our ZMusic wrapper

  1. In the same class as the main "methods" file generated by the generator above (you can use partial if you want to do this in a separate file), create a static initializer.
  2. The initializer should call NativeLibrary.SetDllImportResolver(typeof(<your type>).Assembly, ImportResolver);, and should take steps to ensure that this is not called twice.
  3. Create a method with the signature private static IntPtr ImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
  4. In the body of that method, take whatever steps are needed to locate the library you intend to load. If the file is found, use NativeLibrary.TryLoad():
if (NativeLibrary.TryLoad($"{baseDirectory}{runtimePath}{library}", out m_dllHandle))
{
    return m_dllHandle;
}
  1. If you cannot find or load the library you want, return IntPtr.Zero.
  2. Remember that the file may be named differently depending on target platform. Be sure to use file names appropriate to the platform on which your code is running (NativeLibrary.TryLoad() works the same on all .NET target platforms otherwise). Pay attention to Linux's library-versioning scheme--when the ABI of a library changes, the file name is "incremented", e.g. libmylib.so, libmylib.so.1, libmylib.so.2, libmylib.so.3.