Since the advent of .NET 2.0 and .NET 3.5, Action<T> and Func<T> have been the preferred ways to pass around function signatures, lambdas, and method groups. Where one previously might have declared their own delegate for use in an event or a predicate, we now almost exclusively use these framework-provided generic types - and for good reason.

Using the generic delegates cuts down on boilerplate code, simplifies code reuse, and gives developers further flexibily and integration with other generic constructs.

However, there's been a catch for developers working with native interop code - generics are a complete no-go in the world of DllImport. The problem has been raised both early and late in the lifecycle of the .NET's current iterations. The latest offical discussion I could find on the topic was in an issue on the coreclr repo from 2015.

From version 2.1 and forward, ADL supports marshalling generic delegates in the Action and Func family completely transparently, just as if the runtime itself supported it. That is, you can now declare an interface akin to this one

public interface IGenericDelegateLibrary
{
    void ExecuteAction(Action action);

    void ExecuteActionT1(Action<int> action);

    int ExecuteFuncT1(Func<int> func);

    int ExecuteFuncT1T2(Func<int, int> func);

    Action GetNativeAction();

    Action<int> GetNativeActionT1();

    Func<int> GetNativeFuncT1();

    Func<int, int> GetNativeFuncT1T2();
}

and it's going to Just Work (tm). I thought I'd go into a few of the implementation details and discuss some limitations.

Firstly, it's important to understand a general overview of how ADL implements its extensions to traditional DllImport. Principially, ADL generates a completely new type (at runtime or at compile time) that contains all of the boilerplate you yourself would normally write, cutting out that step in your development process. This type lives in a dynamic assembly, which ADL can modify as it pleases.

Each method in an interface is pushed through a pipeline of stages, where each stage can augment the dynamic assembly with new types, new methods, new algorithms - anything it needs to achieve its purpose.

Taking a look at the source code, it's a bit like this:

/// <summary>
/// Consumes a set of definitions, passing them through the given pipeline. Each stage is guaranteed to run only
/// once for any given branch of the input definitions. The generation process follows a recursive depth-first
/// reductive algorithm.
/// </summary>
/// <param name="definitions">The definitions to process.</param>
/// <param name="pipeline">A sorted list of generators, acting as the process pipeline</param>
/// <typeparam name="T">The type of definition to process.</typeparam>
private void ConsumeDefinitions<T>
(
    [NotNull] IEnumerable<PipelineWorkUnit<T>> definitions,
    [NotNull] IReadOnlyList<IImplementationGenerator<T>> pipeline
)
    where T : MemberInfo
{
    var definitionQueue = new Queue<PipelineWorkUnit<T>>(definitions);

    while (definitionQueue.Any())
    {
        var workUnit = definitionQueue.Dequeue();
        var definition = workUnit.Definition;

        // Find the entry stage of the pipeline
        var stage = pipeline.First(s => s.IsApplicable(definition));

        // Push the definitions through the stage
        var generatedDefinitions = stage.GenerateImplementation(workUnit).ToList();

        if (!generatedDefinitions.Any())
        {
            continue;
        }

        // Run the new definitions through the remaining stages of the pipeline
        ConsumeDefinitions(generatedDefinitions, pipeline.Except(new[] { stage }).ToList());
    }
}

The pipeline continually processes members from the interface, pushing them through the stages until no more members remain. A powerful capability of the individual stages is not only their ability to alter the signature of a method, but also to produce multiple new methods - this is how we implement nullable structs by reference, for instance.

The resulting implementation of a stage does some magic, calls the implementation of the next stage, which in turn does some magic, calling the implementation of the next stage, and so on until we reach a terminating stage that emits the final binding to native code.

Each stage puts the member through four primary phases, in order:

  • Emitting additional types
  • Emitting the signature that will be passed to the next stage
  • Emitting IL instructions placed before the call to the passed-through signature
  • Emitting IL instructions placed after the call to the passed-through signature

This combination of phases lets each stage do pretty much anything it wants or needs to do in order to achieve its purpose.

Before the advent of the generic delegate wrapper, the first phase did not exist

  • no previous stages had required creating new types. With it it place, however, I'm sure we can find more fascinating uses for it.

The generic delegate wrapper stage itself is deceptively simple. With this already solid system in place, it didn't take more than two hours to go from concept to working implementation. The principle, like pretty much everything in ADL, is based on the concept of Compiler Lowering.

P/Invoke has always been able to handle marshalling user-defined delegates without a hitch, as long as they don't involve generics. Function pointers can be marshalled back and forth and executed on both sides of the managed fence with very little trouble (save for having to keep your delegates alive somehow).

When the wrapper stage encounters a signature that contains a generic delegate, instead of passing it through to the runtime as-is, it generates a new delegate type where the type arguments are statically compiled, and replaces the parameter with this new type. Take this simple function.

bool PerformSomeComputation(Func<int, int> computation);

After the stage is done with it, it becomes this,

public delegate int FuncImplementation(int p1);
bool PerformSomeComputation(FuncImplementation computation);

which we can pass through to the runtime and let it handle the rest. Corollary, approaching it from the other way, we can take an explicit delegate type from the CLR's marshalling system and turn it back into a generic delegate before we hand it back to the user.

Our initial interface function's implementation becomes the following, passing on and transforming its parameter to the "lower" method.

public bool PerformSomeComputation(Func<int, int> computation)
{
    return PerformSomeComputation(new FuncImplementation(computation));
}

This approach is a little naive, of course, but works very well for generic delegates where the types are known at compile time.

Its primary limitation is that the type arguments of the generic delegates must be fully resolved at compile time, that is, you can't declare a type argument on the method itself and pass it to the delegate - it must be fully resolved without having to call the method.

It also doesn't handle nested generics very well. It does have it implemented, but the results are spotty since it relies on the way the CLR treates nested explict delegates in P/Invoke. Marshalling nested generics to unmanaged from managed works (on Mono, but not .NET Core), but it's prone to blowing up in your face, eating small pets, and setting fire to houseplants. I've left it in for you to play with.

The approach can be extended to types only known at the callsite for more, ahem, generic generics, but I haven't explored that avenue yet. I'd wager it would involve a bit of per-call method resolution and new method generation which, while not impossible, is a whole can of worms.

Perhaps something better explored in the CLR itself, eh?

ADL 2.1 is available on GitHub, MyGet, and NuGet.