# IL Post Process (ILPP) in Unity: How Ceres Eliminates Runtime Reflection for Maximum Performance

> Learn how IL Post Process in Unity eliminates runtime reflection for peak performance. Ceres uses this compilation hook for faster initialization and optimized code.

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

---

**IL Post Process (ILPP)** is a Unity compilation pipeline hook that manipulates Intermediate Language (IL) after the C# compiler finishes but before the assembly is written to disk, allowing the Ceres framework to generate initialization code at compile time and eliminate expensive runtime reflection.

The **akikurisu/ceres** repository leverages IL Post Processing to transform node-based gameplay logic into high-performance IL that avoids allocations and virtual dispatch. By operating on the assembly via Mono.Cecil during the build phase, Ceres converts reflection-heavy operations into direct method calls and field accesses.

## What is IL Post Process (ILPP)?

**IL Post Processing** is a Unity Editor feature that intercepts compiled assemblies before they are packaged into the final build. Implementing the `ILPostProcessor` interface allows frameworks to rewrite IL instructions using libraries like Mono.Cecil, enabling compile-time code generation that would otherwise require slow reflection at runtime.

In Ceres, the ILPP system lives in the `Unity.Ceres.ILPP.CodeGen` namespace and executes automatically whenever Unity compiles the `Ceres.dll` assembly. This process happens transparently during development, meaning developers write standard C# node classes while the ILPP infrastructure injects performance-critical boilerplate behind the scenes.

## The Three Core ILPP Processors in Ceres

Ceres implements three specialized processors that work together to optimize node initialization and execution. Each processor targets specific performance bottlenecks in the node graph architecture.

### RuntimeAccessModifiersILPP

Located in [`Runtime/CodeGen/RuntimeAccessModifiersILPP.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/CodeGen/RuntimeAccessModifiersILPP.cs), this processor modifies the accessibility of critical `CeresNode` fields. It changes `SharedVariables`, `Ports`, and `PortLists` from `internal` to `protected internal` by setting `IsFamilyOrAssembly` to true on their FieldDefinition.

This accessibility shift allows generated initialization code to access these collections directly without exposing them publicly in the source API. The modification happens without altering the original C# source files, maintaining clean encapsulation while enabling IL-level optimizations.

### CeresNodeILPP

The [`Runtime/CodeGen/CeresNodeILPP.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/CodeGen/CeresNodeILPP.cs) processor performs the heavy lifting of node initialization. For every subclass of `CeresNode` discovered via `type.IsSubclassOf(CodeGenHelpers.CeresNode_FullName)`, it generates three critical overrides:

- **`__initializeVariables`** – Allocates `SharedVariable` fields only when null and registers them in the `SharedVariables` list
- **`__initializePorts`** – Instantiates `CeresPort` fields and populates the `Ports` dictionary with name-to-instance mappings
- **`__getTypeName`** – Returns a constant string representing the concrete type name, eliminating virtual dispatch overhead

These generated methods use conditional allocation (`if (field == null) field = new T()`) to prevent unnecessary heap allocations during node reuse.

### ExecutableReflectionILPP

Found in [`Runtime/CodeGen/ExecutableReflectionILPP.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/CodeGen/ExecutableReflectionILPP.cs), this processor eliminates reflection from executable flow-graph functions and implementable events. It rewrites event handler invocations that would normally use `MethodInfo.Invoke` into direct method calls.

By resolving method references at compile time and emitting direct IL instructions, this processor removes the costly reflection pipeline entirely. The resulting code executes as fast as standard virtual method calls while maintaining the flexibility of the original event architecture.

## How IL Post Process Enhances Runtime Performance

The IL Post Process pipeline delivers measurable performance improvements through five specific optimizations:

**1. Compile-Time Code Generation**
Instead of using reflection at runtime to discover `SharedVariable` and `CeresPort` fields via `GetFields()`, Ceres generates concrete `__initializeVariables` and `__initializePorts` methods during the build. This transforms runtime type inspection into simple method invocations that the JIT compiler can inline.

**2. Allocation-Free Initialization**
The generated IL only instantiates objects when fields are `null`, avoiding repeated heap allocations in tight gameplay loops. This pattern ensures that resetting or reusing nodes does not trigger garbage collection pressure.

**3. Eliminated Virtual Dispatch**
By overriding `__getTypeName` to return a constant string rather than relying on `GetType().Name` or hash lookups, Ceres enables the runtime to resolve type identity with zero overhead. The method becomes a sealed accessor returning a pre-computed literal.

**4. Protected Member Access Without Source Pollution**
`RuntimeAccessModifiersILPP` enables generated code to access internal node state without making fields public or modifying the original source. This maintains API cleanliness while allowing the IL-level optimizations to function.

**5. Reflection-Free Event Handling**
`ExecutableReflectionILPP` converts `MethodInfo.Invoke` calls into direct IL call instructions. This removes the expensive reflection binding and boxing overhead typically associated with dynamic event invocation in Unity.

## The Ceres ILPP Architecture Flow

The IL Post Process execution follows a precise pipeline from compilation to final assembly:

1. **Compilation Phase** – Unity compiles C# source files into the `Ceres.dll` assembly

2. **ILPP Discovery** – Unity's build pipeline identifies classes implementing `ILPostProcessor` in the `Unity.Ceres.ILPP.CodeGen` namespace
3. **Assembly Loading** – `CodeGenHelpers.AssemblyDefinitionFor` loads the compiled assembly into Mono.Cecil objects for manipulation
4. **Module Resolution** – `CodeGenHelpers.FindBaseModules` locates the Unity core module and Ceres module to resolve type references
5. **Type Walking** – Each processor iterates type definitions:
   - `CeresNodeILPP` identifies node subclasses and injects initialization logic
   - `RuntimeAccessModifiersILPP` toggles accessibility flags on target fields
   - `ExecutableReflectionILPP` rewrites method bodies to remove reflection
6. **Assembly Write-Back** – The modified assembly and PDB are written to memory and returned to Unity for final packaging

Any errors or warnings during this process are collected in the `m_Diagnostics` list and surfaced in the Unity Console, providing clear feedback when code generation fails.

## IL Post Process Code Generation Example

### Before ILPP: User-Defined Node

Developers write clean, attribute-free C# classes:

```csharp
public class MyNode : CeresNode
{
    public SharedVariable<int> counter;
    public CeresPort output;
}

```

### After ILPP: Generated Implementation

The IL Post Process injects the following logic into the assembly (visible via IL inspection):

```csharp
protected internal List<SharedVariable> SharedVariables;
protected internal Dictionary<string, CeresPort> Ports;

protected override void __initializeVariables()
{
    if (counter == null) 
        counter = new SharedVariable<int>();
    SharedVariables.Add(counter);
    base.__initializeVariables();
}

protected override void __initializePorts()
{
    if (output == null) 
        output = new CeresPort();
    Ports.Add(nameof(output), output);
    base.__initializePorts();
}

protected internal override string __getTypeName()
{
    return "MyNode";
}

```

### Runtime Usage: Zero Reflection

At runtime, the Ceres engine invokes these generated methods directly:

```csharp
var node = new MyNode();
node.__initializeVariables();  // Direct call, no reflection
node.__initializePorts();      // Direct call, no reflection

Debug.Log(node.counter.Value); // Direct field access

```

## Summary

- **IL Post Process (ILPP)** manipulates assembly IL after C# compilation but before disk write, enabling compile-time optimizations in Unity.

- Ceres uses three processors—`RuntimeAccessModifiersILPP`, `CeresNodeILPP`, and `ExecutableReflectionILPP`—located in `Runtime/CodeGen/` to transform node classes.
- The system eliminates runtime reflection by generating `__initializeVariables`, `__initializePorts`, and `__getTypeName` methods during the build.
- Performance gains include allocation-free initialization, eliminated virtual dispatch, and reflection-free event handling.
- All transformations occur via Mono.Cecil during the Unity build pipeline, requiring no runtime overhead or source code pollution.

## Frequently Asked Questions

### What does IL Post Process modify in Unity?

IL Post Process modifies the compiled Intermediate Language (IL) of managed assemblies after the C# compiler generates them but before Unity packages them into the build. In Ceres, it modifies `Ceres.dll` to inject initialization methods and adjust field accessibility using Mono.Cecil, transforming the assembly without changing the original source files.

### How does ILPP improve performance compared to reflection?

ILPP improves performance by shifting work from runtime to compile time. Instead of using `GetFields()` and `MethodInfo.Invoke` during gameplay—which involves expensive metadata lookups and boxing—Ceres generates direct IL instructions that the JIT compiler can optimize and inline. This eliminates allocation overhead and virtual dispatch costs entirely.

### Which Ceres components rely on IL Post Processing?

The node initialization system relies on `CeresNodeILPP` to set up `SharedVariable` and `CeresPort` instances, while `RuntimeAccessModifiersILPP` enables access to the `SharedVariables`, `Ports`, and `PortLists` collections. Additionally, `ExecutableReflectionILPP` optimizes the executable graph flow by removing reflection from event invocations.

### Is ILPP specific to Ceres or a general Unity feature?

IL Post Processing is a general Unity Editor feature available to any package or project. Ceres implements specific `ILPostProcessor` subclasses to customize the pipeline for its node architecture. Other Unity packages like Entities and Netcode for GameObjects also use ILPP for similar compile-time code generation tasks.