2023-03-02 13:00:00
Delegates
This is part of a series on Native AOT.
Previous -- Top -- Next
Developing with .NET often involves delegates, which we can think of as objects that represent things that are callable. For example:
public static int count_files_with_e(string path)
{
return System.IO.Directory.GetFiles(path)
.Where(x => x.Contains("e"))
.Count()
;
}
The extension method Where
accepts a delegate. We're calling
it with a lambda expression, which the compiler
converts into the correct delegate type.
How do we deal with delegates in a Native AOT library?
The signature for System.Linq.Enumerable.Where()
is fairly hard on the eyes:
public static IEnumerable<TSource> Where<TSource>( this IEnumerable<TSource> source, Func<TSource,bool> predicate );
All those generics! Yikes. We need to start with a simpler example:
public delegate int MapOne(int x);
public static int MapSum(
int n,
MapOne f
)
{
var sofar = 0;
for (var i=0; i<n; i++)
{
sofar += f(i);
}
return sofar;
}
The delegate type
MapOne
is a simple mapping of one integer value to another.The
MapSum
function loops over the firstn
integers and applies a delegate to each one, returning the sum of the results.
Calling MapSum
from C#
with a lambda expression might look like this:
public static int CountDivisibleBy42(int n) { return MapSum( n, x => ((x % 42) == 0) ? 1 : 0 ); }
But functions exposed by a Native AOT library must follow the rules of C, and C doesn't have delegates -- it has function pointers:
typedef int (*MapOne)(int);
For the sake of illustration, let's observe that
C# 9 added support for function pointers, so one option
here is to
just rewrite MapSum
to use them:
[UnmanagedCallersOnly(EntryPoint = "map_sum_with_funcptr")]
public static unsafe int MapSumWithFuncPtr(
int n,
delegate* unmanaged<int,int> f
)
{
var sofar = 0;
for (var i=0; i<n; i++)
{
sofar += f(i);
}
return sofar;
}
This results in a function signature which is compatible with Native AOT, so we could call it from C. First, since C doesn't have lambdas, we need to define the map function:
int divisible_by_42(int x) { return ((x % 42) == 0) ? 1 : 0; }
And the call from C to the Native AOT function looks like this:
int32_t total = map_sum_with_funcptr( 1000, divisible_by_42 ); printf("%d\n", total);
But Native AOT libraries won't be much fun
if we need to rewrite everything.
It would be preferable to leave MapSum
unchanged and
still provide a way to call it. In other words,
we want to convert our function pointer into a
delegate. We can do that with System.Delegate.CreateDelegate()
.
The .NET class libraries provide CreateDelegate
as
a way to create delegates of a given type from other methods.
It has several overloads, but none of them accept a function
pointer, so we need to wrap our function pointer in a something
that CreateDelegate
can accept. I call this
wrapper a "shadow" class:
private unsafe class my_shadow
{
readonly delegate* unmanaged<int,int> _funcptr;
public my_shadow(delegate* unmanaged<int,int> funcptr)
{
_funcptr = funcptr;
}
public unsafe int Invoke(int x)
{
return _funcptr(x);
}
}
Now we have a regular C# object that contains the
function pointer and provides a method to invoke it.
So we can create a delegate of type MapOne
by making an instance of that shadow class and passing it
to CreateDelegate
:
var shadow = new my_shadow(funcptr);
var del = Delegate.CreateDelegate(
typeof(MapOne),
shadow,
typeof(my_shadow).GetMethod("Invoke")
);
Note that some of the overloads for CreateDelegate
cause AOT or trimmer warnings. I'm using an overload that
Native AOT likes.
Using the GCHandle
approach described previously,
we can return the delegate object across the Native AOT
boundary so it can be passed back, and for that purpose we
need to expose our original MapSum
function with a signature
that accepts that delegate object handle:
[UnmanagedCallersOnly(EntryPoint = "map_sum_with_delegate")] public static int MapSumWithDelegate( int n, IntPtr del ) { var actualDelegate = (MapOne) GCHandle.FromIntPtr(del).Target; return MapSum(n, actualDelegate); }
And finally, we can call MapSum
from C like this:
intptr_t del = create_delegate( divisible_by_42 ); int32_t total = map_sum_with_delegate( 1000, del ); printf("%d\n", total);
The code for this blog entry is available at:
https://github.com/ericsink/native-aot-samples/tree/main/delegate_i32