Skip to content

Function pointers C# 9.0performanceinterop

Call methods through pointers without delegate allocations in performance-critical code.

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 unsafe contexts
  • 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* unmanaged must 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

More information ​

Powered by Intracia