An Introduction to ADL (or how to double your native .NET interop performance)

Jarl Gullberg
6 min readApr 14, 2018

--

“Close-up of a laptop screen with lines of code” by Artem Sapegin on Unsplash. It’s surprisingly difficult to find header photographs of anything except CSS or JS.

In today’s modern, cross-platform .NET world we’re swimming in unprecedented support for different operating systems and processor architectures. It’s a bonanza of new and exciting technology and opportunities — one that is partially soured for mixed developers who want or have to work in both the cozy, comfy managed world of .NET and the performance-critical wild west of low-level code.

It might be surprising to hear that P/Invoke hasn’t changed much — if at all — since its introduction in .NET 1.1. Developers wishing to leverage the power and freedom of low-level programming in C# are locked to either using static classes and DllImport attributes, or building their own ramshackle solutions with delegates and Marshal.GetDelegateForFunctionPointer(IntPtr ptr, Type delegateType).

Unfortunately, both of these solutions have their individual issues — lack of flexibility, performance overhead, reliance on compile-time library names — the list goes on.

To solve most — if not all — of these issues, a friend (BlackCentipede) and I have developed a new solution for native interop in the CLR — AdvancedDLSupport (or ADL, for short).

The library was created with three things in mind: flexibility, modernity, and speed. It takes a new approach to binding to native code, using familiar tools in a new way. Furthermore, it targets .NET Standard 2.0, giving it wide compatibility with existing projects and runtimes.

The library is available for free on Github and Nuget — read on to see how you can use it to simplify your life with P/Invoke.

Table of Contents

  1. Basic Usage
  2. Mixed-Mode Classes
  3. Under the Hood
  4. Delegate-based Binding
  5. Indirect Calls
  6. Performance

Basic Usage

Let’s take this simple C library.

math.h

math.c

In your typical DllImport-driven interop, you might declare a static class like this, and import the functions from the library.

Old and grubby.

This is all well and good, but you’re now faced with some annoying constraints. In order to use this on multiple platforms, you are forced to rely on platform-specific logic to resolve the location of your library: math.dllon Windows, and libmath.soor libmath.dylibon *nix and macOS.

Additionally, the class is static, and is difficult to use in modern scenarios; because of its static nature, the class can’t be passed around, it can’t be instantiated, it can’t inherit from any class, other classes cannot inherit from it, etc. Finally, it’s slow. DllImport carries some overhead, which can be painfully noticeable in high-churn applications.

ADL takes a different approach. Instead of declaring a class, we declare an interface.

New and improved!

Using this interface, we can then instantiate a type that implements the interface, and binds to the native functions. Of note is the property, which will bind to a global variable — something DllImport can’t do at all.

This has several benefits.

  1. You are in complete control of when and how to load and unload the native functions.
  2. You decide, at runtime, the location or name of your library.
  3. The library is now an instance, and is not static. You can inject it into your types, create mocks for unit tests, use it for generic constraints, etc.

Beyond basic usage like this, ADL supports all the typical P/Invoke patterns and mechanisms (passing structs by ref or by value, StringBuilder, passing classes, etc), outlined in the documentation.

ADL also brings some refreshing new features to the table:

  1. Mixed-mode classes
  2. Per-symbol disposal checks
  3. Integrated support for Mono’s DllMap mechanism
  4. Lazy loaded symbols
  5. Support for T?and ref T?parameters as first-class citizens
  6. Support for binding to global variables

These are covered in ADL’s advanced configuration documentation, but mixed-mode classes are quite interesting on their own. Let’s take a look.

Mixed-Mode Classes

Beyond simple interfaces, ADL also allows you to seamlessly mix managed and native code via the use of mixed-mode classes. In short, you can have a managed class implement an unmanaged interface, making the native methods available right inside your own code.

Using the previously outlined native library, we can create a class in similar vein to this:

As you can see, managed functions can coexist with unmanaged ones, and managed code can override implementations of the unmanaged functions — as seen in Subtract. This allows you to more easily wrap object-oriented managed code, or provide mixed access to your native code. Perhaps you want to have more detailed input verification, or perhaps you want to isolate certain portions of your code.

In order to create a mixed-mode class, simply declare an abstract class that inherits from NativeLibraryBase, and implements the interface you want to activate. Any interface members can have explicit managed implementations, or remain abstract to be routed to their corresponding native implementations.

There are a few limitations:

  • Mixed-mode class must inherit from NativeLibraryBase
  • Mixed-mode classes must be abstract
  • Properties may only be fully managed or fully unmanaged — no mixing of getters and setters

Once you have your class definition, instances of it can be created in much the same way as you would interface instances:

The produced class will inherit from the base class, and implement the given native interface.

Under the hood

ADL leverages some known techniques and some more arcane approaches to enable flexible and efficient binding to native libraries.

At its core, ADL inspects the interface you pass it, and generates a new type at runtime that implements the interface, forwarding calls to the interface methods to their native counterparts.

First and foremost, it uses the native platform’s method to load dynamic libraries at runtime, and look up unmanaged function pointers from them. On Unix and BSDs, this means libdl, and on Windows, the LoadLibrary/GetProcAddressmethods from kernel32.

After this, depending on the way you configure it, there are two primary paths it takes.

Delegate-based Binding

In C#, there exist methods to take an unmanaged function pointer and turn it into a delegate — Marshal.GetDelegateForFunctionPointer(IntPtr ptr, Type delegateType). This lies at the core of ADL’s delegate-based approach, and it generates a matching delegate type for your methods.

This method of binding to native code is flexible, but is unfortunately quite slow.

It’s simple, but reliable. All of this can be done by hand, of course, but it ends up being a cumbersome amount of boilerplate code that has to be written and then maintained. ADL, being that it generates this automatically, saves you a significant chunk of time and maintenance costs.

Indirect Calls

If, however, you want to squeeze some extra speed out of your interop, ADL also offers another way to bind — by using the calliopcode. This is a fairly unknown opcode in the CLR, but it’s been used with great success by large projects like OpenTK and SharpDX to speed up their native interop.

calli, in a nutshell, directly calls an unmanaged function pointer described by a callsite, bypassing all of the overhead that stems from type checking, delegate generation, and runtime code verification.

Instead of generating a delegate, we can simply call the symbol pointer directly.

This way of calling the unmanaged pointer produces massive speed benefits, resulting in between 2 and 8 times the speed of normal DllImportor delegates.

It is, however, not without its faults. Code with calliis inherently unverifiable, and will not run under partial trust on Windows. However, the default security policy is to run executables with full trust — so unless you have a restricted platform, it won’t be an issue.

Additionally, .NET Core lacks a way to set the unmanaged calling convention at runtime, resulting in unreliable results when running as a 32-bit process on Windows. This is, fortunately, a very uncommon configuration.

This method is normally not available to developers — the various CLR compilers (C#, F#, VB.NET) never emit the calliopcode on their own.

Performance

So we’ve been banging on about performance for a while now — let’s see some numbers. This is a benchmark of Matrix2inversions performed using BenchmarkDotNet, targeting ADL, managed code, and traditional DllImport. The tests have been run under Mono, .NET Core, and the full .NET Framework (v4.7.1).

The Mono and .NET Core tests were performed on Linux Mint 18.3, using an i7–4790K with 16GB RAM.

The full FX tests were performed on Windows 10, using an i7–7600K with 16GB RAM.

Each test case is as follows:

Managed                       : Managed code, no interop
DllImport : Traditional DllImport
Delegates : Delegates, with disposal checks
DelegatesWithoutDisposeChecks : Delegates, no disposal checks
calli : Using the calli opcode

Mono

.NET Core

.NET FX

Across all platforms, calli remains quite consistent, while delegates and DllImportsee some rather worrying fluctuations. Delegates on Mono appears to be an outlier and is significantly slower than other methods.

You can view the source code of and run the benchmark yourself here: AdvancedDLSupport.Benchmark

ADL is available for free on Github and Nuget. For companies, established open-source projects, or individuals wanting a custom license, please chuck us an email.

Originally published at sharkman.asuscomm.com on April 13, 2018.

--

--