# How Ceres Achieves Near-Native Performance with IL2CPP Optimization for Function Calls

> Discover how Ceres uses IL2CPP optimization to achieve near-native performance for function calls in Unity. Learn about native method pointer resolution and unmanaged function pointers for C++ level performance.

- Repository: [AkiKurisu/ceres](https://github.com/akikurisu/ceres)
- Tags: performance
- Published: 2026-02-24

---

**Ceres eliminates reflection overhead in Unity IL2CPP builds by resolving native method pointers through `il2cpp_class_get_method_from_name` and invoking them via C# unmanaged function pointers, achieving C++-level call performance for flow-graph nodes.**

The **akikurisu/ceres** repository implements a high-performance visual scripting runtime for Unity that must execute user-defined functions with minimal overhead. When targeting IL2CPP—the Ahead-of-Time (AOT) compilation backend used by Unity for consoles and mobile devices—Ceres leverages a specialized **IL2CPP optimization** path that bypasses the expensive reflection and delegate creation typically required for dynamic method invocation.

## The Two-Step IL2CPP Optimization Strategy

Ceres implements a dual-phase approach to transform managed method calls into direct native calls. This strategy is conditionally compiled behind `#if ENABLE_IL2CPP` blocks, ensuring the same codebase remains compatible with both Mono and IL2CPP runtimes.

### Step 1: Discovering the Native Method Address

When a flow-graph node references a C# instance method, Ceres must locate the exact native entry point generated by the IL2CPP compiler. This occurs in [`Runtime/Flow/Models/ExecutableReflection.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/ExecutableReflection.cs) during function registration.

The system queries the IL2CPP runtime through the thin wrapper `IL2CPP.GetIl2CppMethod`, defined in [`Runtime/Core/Models/IL2CPP.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Core/Models/IL2CPP.cs). This wrapper internally invokes the IL2CPP C API function **`il2cpp_class_get_method_from_name`**, passing the class pointer (`_il2cppClass`) and constructing the method name `Invoke_<FunctionName>`—the IL2CPP-generated invoker that matches the original C# signature.

```csharp
// Simplified excerpt from ExecutableReflection<T>.GetFunction_Internal
#if ENABLE_IL2CPP && (UNITY_STANDALONE_WIN || UNITY_ANDROID)
unsafe {
    if (functionType == ExecutableFunctionType.InstanceMethod && _il2cppClass != IntPtr.Zero) {
        int invokeParameterCount = functionInfo.ParameterCount >= 0
            ? functionInfo.ParameterCount + 1   // 'this' pointer + arguments
            : -1;
        var ptr = IL2CPP.GetIl2CppMethod(
            _il2cppClass,
            $"Invoke_{functionName}",
            invokeParameterCount);
        // Native pointer stored for later invocation
    }
}
#endif

```

### Step 2: Direct Invocation via Function Pointers

Once discovered, the native address (`IntPtr`) is cached within an `ExecutableFunction` instance. The actual invocation logic resides in [`Runtime/Flow/Models/ExecutableAction.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/ExecutableAction.cs), where `ExecutableAction<TTarget>` overloads check for the presence of this cached pointer.

When `_functionPtr` is non-zero, Ceres casts it to an unmanaged function pointer using C# 9.0 function pointer syntax (`delegate*`) and executes a direct call. This bypasses the .NET delegate system entirely, resulting in a single instruction jump to the compiled native code.

```csharp
public void Invoke<T1>(TTarget target, T1 arg1) {
    if (IsStatic) {
        if (_functionPtr != IntPtr.Zero) {
            ((delegate*<T1, void>)_functionPtr)(arg1);   // Direct native call
            return;
        }
    }
#if ENABLE_IL2CPP
    if (_functionPtr != IntPtr.Zero) {
        ((delegate*<TTarget, T1, void>)_functionPtr)(target, arg1); // Direct native call
        return;
    }
#endif
    // Fallback to reflection-based delegate invocation
}

```

## Why This Approach Eliminates Reflection Overhead

The **IL2CPP optimization** in Ceres avoids three major performance bottlenecks associated with dynamic invocation:

- **Reflection elimination**: The system never calls `MethodInfo.Invoke`, avoiding metadata lookups and boxing of arguments.
- **Delegate allocation bypass**: Unlike the Mono fallback path that uses `Delegate.CreateDelegate`, the IL2CPP path requires no managed delegate object allocation.
- **Pre-generated marshalling**: IL2CPP generates `Invoke_<Name>` wrappers at build time that handle `this` pointer passing and argument marshalling exactly as the native signature expects, eliminating JIT-style conversion overhead.

The result is that flow-graph function calls in IL2CPP builds exhibit the same performance characteristics as hand-written C++ function calls, with zero per-call lookup costs after the initial registration phase.

## Implementation Details in the Ceres Source Code

The optimization spans three critical files in the repository:

1. **[`Runtime/Core/Models/IL2CPP.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Core/Models/IL2CPP.cs)**: Contains the P/Invoke definitions exposing the IL2CPP C API and the `GetIl2CppMethod`/`GetIl2CppClass` helper utilities.
2. **[`Runtime/Flow/Models/ExecutableReflection.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/ExecutableReflection.cs)**: Handles function registration and resolves IL2CPP method pointers during setup, storing them in `ExecutableFunction` instances.
3. **[`Runtime/Flow/Models/ExecutableAction.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/ExecutableAction.cs)**: Implements the execution logic, selecting the fast native-pointer path when available (starting at line 604 in the repository).

Registration occurs once per function during graph initialization:

```csharp
// From ExecutableReflection<T>.RegisterExecutableFunction
var functionInfo = new ExecutableFunctionInfo(
    ExecutableFunctionType.InstanceMethod,
    methodInfo.Name,
    methodInfo.GetParameters().Length);
var functionStructure = new ExecutableFunction(functionInfo, methodInfo);
_functions.Add(functionStructure);

```

## Summary

- Ceres achieves **near-native performance** by resolving IL2CPP method pointers at registration time using `il2cpp_class_get_method_from_name`.
- Invocation uses C# unmanaged function pointers (`delegate*`) to execute direct native calls without reflection or delegate overhead.

- The optimization is conditionally compiled for IL2CPP targets while maintaining fallback compatibility with standard Mono reflection.
- Critical files include [`IL2CPP.cs`](https://github.com/akikurisu/ceres/blob/main/IL2CPP.cs) for API binding, [`ExecutableReflection.cs`](https://github.com/akikurisu/ceres/blob/main/ExecutableReflection.cs) for pointer resolution, and [`ExecutableAction.cs`](https://github.com/akikurisu/ceres/blob/main/ExecutableAction.cs) for fast-path execution.

## Frequently Asked Questions

### How does Ceres handle method calls when IL2CPP is not enabled?

When `ENABLE_IL2CPP` is undefined, Ceres falls back to standard .NET reflection. It creates cached delegates using `Delegate.CreateDelegate` from `MethodInfo` objects. While this is significantly slower than the native pointer approach, it maintains functional compatibility across all Unity platforms.

### What is the `Invoke_<FunctionName>` pattern used in the IL2CPP lookup?

IL2CPP generates C++ wrapper functions for every managed method to handle calling conventions and `this` pointer marshalling. These follow the naming convention `Invoke_<OriginalMethodName>`. Ceres constructs this string dynamically to locate the correct native entry point through the IL2CPP runtime API.

### Does this optimization work on all Unity platforms?

The current implementation specifically targets `UNITY_STANDALONE_WIN` and `UNITY_ANDROID` as indicated by the preprocessor conditions in the source. Other platforms may require additional P/Invoke definitions or IL2CPP API availability verification in [`Runtime/Core/Models/IL2CPP.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Core/Models/IL2CPP.cs).

### Why use `delegate*` instead of standard `Marshal.GetDelegateForFunctionPointer`?

`delegate*` (C# function pointers) generate direct call instructions at the IL level without allocating a delegate object on the managed heap. This reduces memory pressure and eliminates the indirection layer that standard delegates require, resulting in the "near-native" performance characteristic that the optimization targets.