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

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, 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 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, 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 LoadingCodeGenHelpers.AssemblyDefinitionFor loads the compiled assembly into Mono.Cecil objects for manipulation

  4. Module ResolutionCodeGenHelpers.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:

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):

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:

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →