Delegates provide type-safe method references but incur allocation and indirection overhead. For performance-critical scenarios and native interop this overhead can be significant, but C# had no way to call through a raw function pointer.
C# 9.0 introduces function pointers using the delegate* syntax in unsafe contexts. They compile to direct IL function pointer calls without delegate allocations, making them ideal for high-performance scenarios, game engines, and native interop.
Code
csharp
unsafe class Calculator
{
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
static int Compute(int x, int y, delegate*<int, int, int> operation)
{
return operation(x, y);
}
public static void Main()
{
delegate*<int, int, int> addPtr = &Add;
delegate*<int, int, int> mulPtr = &Multiply;
int sum = Compute(5, 3, addPtr); // 8
int product = Compute(5, 3, mulPtr); // 15
// Can also call directly
int result = addPtr(10, 20); // 30
}
}
// P/Invoke scenario
unsafe static class Native
{
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
static void OnValue(int value) => Console.WriteLine(value);
[DllImport("native.dll")]
static extern void ProcessCallback(delegate* unmanaged[Cdecl]<int, void> callback);
public static void Register() => ProcessCallback(&OnValue);
}csharp
class Calculator
{
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
static int Compute(int x, int y, Func<int, int, int> operation)
{
return operation(x, y); // Delegate invocation overhead
}
public static void Main()
{
Func<int, int, int> addFunc = Add; // Allocation
Func<int, int, int> mulFunc = Multiply; // Allocation
int sum = Compute(5, 3, addFunc);
int product = Compute(5, 3, mulFunc);
}
}
// P/Invoke required a marshaled, non-generic delegate type
static class Native
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void ValueCallback(int value);
// Must be kept alive so the GC does not collect it while native code holds it
static readonly ValueCallback onValue = OnValue;
static void OnValue(int value) => Console.WriteLine(value);
[DllImport("native.dll")]
static extern void ProcessCallback(ValueCallback callback);
public static void Register() => ProcessCallback(onValue);
}Notes
- Function pointers can only be used in
unsafecontexts - Use the
&operator to obtain a pointer to a static method - Syntax:
delegate*<param1Type, param2Type, ..., returnType> - Supports calling conventions:
delegate* unmanaged[Cdecl]<...>,delegate* unmanaged[Stdcall]<...>, etc. - A C# method passed to native code as a
delegate* unmanagedmust be marked[UnmanagedCallersOnly](.NET 5 or later) - No allocations or GC pressure, so they are faster than delegates for repeated calls
- Only static methods can be targeted; pass any instance explicitly as a parameter
- Signatures are checked by the compiler, but nothing verifies the pointer is valid at runtime, which is why they require unsafe code